0%

JVM

1. JVM学习路线

预科知识

JVM Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

JVM运行优点

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

对比 jvm jre jdk

image-20230128150337660

学习路线

JVM学习路线

  1. JVM内存结构
  2. GC
  3. 类文件
  4. 字节码
  5. JMM
image-20230128150845220

常见 JVM

image-20230128150432239

我们讲的以 HotSpot 为主

维基参考

JVM资料参考

2. JVM内存结构

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区

程序计数器

Program Counter Register 程序计数器(通过寄存器实现 –寄存器是CPU读取最快的空间

  • 作用,是记住下一条jvm指令的执行地址
  • ==特点==
    • ==是线程私有的==
    • ==不会存在内存溢出==

作用

basic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
image-20230128152640256

时间片概念:时间片即CPU分配给各个程序的时间,每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的

每个线程都有自己的程序计数器

image-20230128152825298

虚拟机栈

虚拟机栈概念

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
image-20230128153020836

思考

  1. 垃圾回收是否涉及栈内存?垃圾回收不管理栈内存 -> 垃圾回收只回收堆内存,而栈内存在每次栈帧执行完弹栈时都会释放
  2. 栈内存分配越大越好吗?栈内存越大,只能使同一线程函数递归的次数越多,并不是越大越好 -> Linux 1024KB; macOS 1024KB; Windows The default value depends on virtual memory (栈内存越大 -> 线程数越少 因为物理内存是一定的 例如总物理 500M 一个线程 1M 理论上可以有 500个线程;如果每个栈内存 2M 理论上只能有 250个线程运行)
  3. 方法内的局部变量是否线程安全?
    • 如果方法内局部变量**没有逃离方法的作用访问(参数传入,或 return 传出)**,它是线程安全的 int x = 0
    • 如果是局部变量引用了对象,**并逃离方法的作用范围(参数传入,或 return 传出)**,需要考虑线程安全 static int x = 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Demo1_17 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}

public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}

public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}

public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}

参考 Java 参数文档

https://docs.oracle.com/javase/8/

https://docs.oracle.com/en/java/javase/11/

https://docs.oracle.com/en/java/javase/11/tools/java.html

https://docs.oracle.com/en/java/javase/17/

栈内存溢出

栈帧过多导致栈内存溢出
  • 栈帧过多导致栈内存溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Demo1_1 {
public static void main(String[] args) throws InterruptedException {
method1();
}

private static void method1() {
method2(1, 2);
}

private static int method2(int a, int b) {
int c = a + b;
return c;
}
}

public class Demo1_2 {
private static int count;

public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}

private static void method1() {
count++;
method1();
}
}

StackOverFlowError

栈帧过大导致栈内存溢出
  • 栈帧过大导致栈内存溢出

设置栈size

1
-Xss256k
第三方库导致 StackOverFlowError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package cn.itcast.jvm.t1.stack;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

/**
* json 数据转换
*/
public class Demo1_19 {

public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");

Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);

Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);

d.setEmps(Arrays.asList(e1, e2));

// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}

class Emp {
private String name;
@JsonIgnore
private Dept dept;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Dept getDept() {
return dept;
}

public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Emp> getEmps() {
return emps;
}

public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}

线程运行诊断

问题1

cpu 占用过多

定位

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id
    • 可以根据线程id (nid) 找到有问题的线程,进一步定位到问题代码的源码行号

Java占用内存过高

top查看

image-20230129012754941
1
2
3
4
5
6
7
# 查看当前所有线程相对应指标
ps H -eo pid,tid,%cpu

ps H -eo pid,tid,%cpu | grep 32655

# jdk提供
jstack 32655

问题2

程序运行很长时间没有结果

有可能因为线程死锁

1
2
nohup java cn.itcast.jvm.t1.Demo1_3&
nohup: ignoring input and appending output to `nohup.out`

排查线程死锁

1
jstack 32655
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 演示线程死锁
*/
class A{};
class B{};
public class Demo1_3 {
static A a = new A();
static B b = new B();


public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (a) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
synchronized (b) {
synchronized (a) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
}
}

本地方法栈

image-20230129013631960

本地方法接口指 -> 不是由Java代码编写,由于Java代码有一定的限制,不能直接操作OS底层,所以就需要使用 c/c++ 编写的本地方法来与操作OS底层的API对接 -> Java代码可以间接通过本地方法来操作OS,而本地方法运行时使用的内存就是本地方法栈

native修饰的本地方法

堆概念

Heap 堆

  • 通过 new 关键字,创建对象都会使用堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

堆内存溢出

堆内存诊断

  1. jsp工具
    • 查看当前系统中有哪些 java 进程
  2. jmap
    • 查看堆内存占用情况 jmap - heap 进程id –查看某一时刻,类似于快照
  3. jconsole
    • 图形界面的,多功能的监测工具,可以连续监测
  4. jvirsualvm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m
*/
public class Demo1_5 {

public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}

OutOfMemoryError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 演示堆内存
*/
public class Demo1_4 {

public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}
1
2
jmap -heap 18756
jmap -heap <进程id>

jconsole

image-20230129015128973

垃圾回收后,内存占用仍然很高

1
2
# jdk提供
jvisualvm
image-20230129015345868

方法区

方法区概念

方法区概念

方法区是所有Java虚拟机线程共享的区域(此特点类似堆),方法区中存储与类结构相关的信息(类 field, method data, methods, constructions 包含 special methods),同样包含 run-time constant pool,运行时常量池

==定义概念上,方法区是 heap 的一部分,但是具体的厂商在完成不同 JVM 的实现时,不一定会遵循定义概念==

规范并不强制方法区的实现位置 -> jdk8 以前的 hotspot 方法区的实现叫做永久带(就是使用堆内存的一部分)-> jdk1.8 之后的方法区实现换了一种方式叫做元空间(用的就不是堆内存,而是本地内存 -> 直接操作系统内存)-> 不同实现就导致方法区的位置不同

同样方法区也会导致 OutOfMemoryError

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

The following exceptional condition is associated with the method area:

  • If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.

官方定义

组成图

image-20230129090700284
  • jdk1.6 常量池 -> 运行时常量池 -> 包含 StringTable
  • ==jdk1.8 之后,方法区就被移到本地内存中 -> 称为元空间 -> 而 StringTable 则被移到 Heap 堆空间中==

方法区内存溢出

  • 1.8 以前会导致永久代内存溢出
1
2
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
*/
public class Demo1_8 extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 20000; i++, j++) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] code = cw.toByteArray();
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
  • 1.8 之后会导致元空间内存溢出
1
2
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m

案例

  • spring
  • mybatis

运行时常量池

常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

运行时常量池

运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

1
2
3
4
5
6
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}

借助jdk提供的反编译工具 javap -v HelloWorld.class -> -v 显示反编译后的详细信息

1
E:\Todo\note\Software\Java_Note\Code\JavaCode\out\production\JavaCode\Test02> javap -v .\HelloWorld.class

==得到反编译后的详细信息==

主要部分包括类 -> 基本信息,常量池,类方法定义,包含了虚拟机指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
Classfile /E:/Todo/note/Software/Java_Note/Code/JavaCode/out/production/JavaCode/Test02/HelloWorld.class
Last modified 2023-1-29; size 547 bytes
MD5 checksum c9ba0a6bb02e055dc5d4a83327a04608
Compiled from "HelloWorld.java"
public class Test02.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // Test02/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LTest02/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 Test02/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public Test02.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTest02/HelloWorld;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

运行时常量池

运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Constant pool:                                 
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // Test02/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LTest02/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 Test02/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
  • 当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址 -> 内存地址 0xXXXX

StringTable

StringTable –字符串常量池,又称串池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象

public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder()[init初始化].append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
s3 == s4; // false -> s3 是串池中的,而s4是堆内存中new 的,所以是两个对象
s3 == s5; // true -> 字符串常量池就是在编译期间,将所用到的,能直接识别的 String(即 "" 的String) 放入串池中。同时进行编译优化 "a" + "b" 的字符串,同样会在串池中保存 -> "ab" -> 如果串池中已经存在 "ab" 则会直接找到 "ab",而不需要再次放入

System.out.println(s3 == s5);
}
}

==字符串常量池就是在编译期间,将所用到的,能直接识别的 String(即 “” 的String) 放入串池中。同时进行编译优化 “a” + “b” 的字符串,同样会在串池中保存 -> “ab” -> 如果串池中已经存在 “ab” 则会直接找到 “ab”,而不需要再次放入==

字符串延迟加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 演示字符串字面量也是【延迟】成为对象的
*/
public class TestString {
public static void main(String[] args) {
int x = args.length;
System.out.println(); // 字符串个数 2275 断点

System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");
System.out.print("1"); // 字符串个数 2285 断点
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.print("5");
System.out.print("6");
System.out.print("7");
System.out.print("8");
System.out.print("9");
System.out.print("0");
System.out.print(x); // 字符串个数 断点
}
}
image-20230129101855859

面试案例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // heap
String x1 = "cd"; // pool
x2.intern();
// 问,如果调换了【最后两行代码】的位置呢(true),如果换位置后是 jdk1.6呢(false)
System.out.println(x1 == x2); // false

Intern()方法

==将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo1_23 {
// jdk8

// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
// 如果没有放入,则 s2 为 串池中的 字符串,而 s 为堆中的字符串 s2 != s

System.out.println( s2 == x);
System.out.println( s == x );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo1_23 {
// jdk1.6

// ["a", "b", "ab"]
public static void main(String[] args) {
String s = new String("a") + new String("b");

// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
// s 拷贝一份,放入串池

String x = "ab"; // 串池中存在 ab 就直接使用
System.out.println( s2 == x); // true
System.out.println( s == x ); // false -> 1.8 true
}
}

StringTable 特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回 -> 即调用 intern 的对象 与 最终放入串池中的对象是两个不同的对象

StringTable 位置

image-20230129112853529

==jdk1.8 之后 StringTable 转换位置的原因 -> 在jdk1.6 时,StringTable 是 永久带实现 -> 必须等到老年代内存空间不足,才会执行 full gc 来回收,而 StringTable 本身是需要很频繁的使用和回收 -> 而堆 heap 中只需要 Minor GC 就可以回收空间 -> 从而减少StringTable内存的优化==

验证 StringTable 位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
* 在jdk6下设置 -XX:MaxPermSize=10m
*/
public class Demo1_6 {

public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}

/*
jdk1.6
java.lang.OutOfMemoryError: PermGen space

jdk1.8
java.lang.OutOfMemoryError: GC overhead limit exceeded
*/

官方文档

-XX:+UseGCOverheadLimit

Enables the use of a policy that limits the proportion of time spent by the JVM on GC before an OutOfMemoryError exception is thrown. This option is enabled, by default and the parallel GC will throw an OutOfMemoryError if more than 98% of the total time is spent on garbage collection and less than 2% of the heap is recovered. When the heap is small, this feature can be used to prevent applications from running for long periods of time with little or no progress. To disable this option, specify -XX:-UseGCOverheadLimit.

StringTable 垃圾回收机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}

StringTable 性能调优

StringTable 底层是一个 hash 表

  • 调整 -XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池 -> 字符串是否需要重复使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 演示串池大小对性能的影响
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009 --> 48w的单词 分散在 1009 个桶里 -> 平均每需要 12097ms
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000 --> 48w的单词 分散在 20w 个桶里 -> 平均每个桶里 2 个单词,所以只需要 401ms 速度相当快
*/
public class Demo1_24 {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
line.intern();
}
System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
}
}
}

空间换时间

字符串是否需要重复使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 演示 intern 减少内存占用
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/
public class Demo1_25 {
public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if(line == null) {
break;
}
address.add(line.intern());
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
image-20230129121046158

加上入池动作

image-20230129121151640

直接内存

直接内存概念

Direct Memory --直接内存不属于 JVM 内存管理,而是属于OS系统内存,可被JVM调用

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • ==不受 JVM 内存回收管理==

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;

public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}

private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}

private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}

直接内存原理

直接内存原理

常规IO

image-20230129131054638
  • Java代码不能操作系统缓冲区
  • 可以对应操作 Java缓冲区 –缓冲区copy -> 性能开销

直接内存NIO

image-20230129131251850

内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 演示直接内存溢出
*/
public class Demo1_10 {
static int _100Mb = 1024 * 1024 * 100;

public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
// jdk8 对方法区的实现称为元空间
}
}

java.lang.OutOfMemoryError: Direct buffer memory

分配和回收原理

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程 [守护线程] 通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 禁用显式回收对直接内存的影响
*/
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;

/*
* -XX:+DisableExplicitGC 显式的
*/
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
image-20230129131816652
  • 开辟1G直接内存,==执行 GC 会并不会释放直接内存==,而是需要通过调用 Unsafe 来释放直接内存
  • 而 DirectByteBuffer类会主动调用 cleanner[虚引用对象]* 自身会控制 Unsafe 来释放 直接内存*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 直接内存分配的底层原理:Unsafe
*/
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();

// 释放内存
unsafe.freeMemory(base);
System.in.read();
}

public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

JVM调优时会经常使用 -> -XX: +DisableExplicitGC 显示的 -> 使代码中显示的调用 System.gc() 失效

因为 System.gc() 触发的使 full GC -> 从而导致性能的严重降低

但是使用 -XX: +DisableExplicitGC 显示的 禁用了代码层面的 gc,就有可能影响直接内存的回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 禁用显式回收对直接内存的影响
*/
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;

/*
* -XX:+DisableExplicitGC 显式的
*/
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
  • 由于 ByteBuffer 对象任然存活,没有被 GC,那么它所对应的直接内存就也没有被回收
  • 所以使用 -XX: +DisableExplicitGC 显示的 禁用了代码层面的 gc,对其他内存没有影响,但是会影响直接内存的回收
  • 可以用 Unsafe 对象直接手动管理 直接内存

3. 垃圾回收 GC

  1. 判断对象可以回收的机制
  2. 垃圾回收算法
  3. 分代垃圾回收
  4. 垃圾回收器
  5. 垃圾回收调优

对象垃圾回收条件

两种判断方式

  1. 引用计数法
  2. 可达性分析算法
引用计数法
image-20230129133749036

无法处理循环引用问题,AB对象循环引用,导致无法被引用计数法回收内存 -> 从而导致内存泄漏

由于两个对象的引用数不能清零,所以导致 GC 始终无法回收这两个对象

==早期的 python VM 使用了这个 引用计数法 来实现 GC,而 JVM 并没有使用这个方式 GC,而是使用 可达性分析算法实现 GC==

可达性分析算法

可达性分析算法,首先确定肯定不能回收的对象根对象,在GC前,先扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为 GC Root ?

Memory Analyzer(MAT)

The Eclipse Memory Analyzer is a fast and feature-rich Java heap analyzer that helps you find memory leaks and reduce memory consumption.

Use the Memory Analyzer to analyze productive heap dumps with hundreds of millions of objects, quickly calculate the retained sizes of objects, see who is preventing the Garbage Collector from collecting objects, run a report to automatically extract leak suspects.

–Java对象分析工具

演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 演示GC Roots
*/
public class Demo2_2 {

public static void main(String[] args) throws InterruptedException, IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1);
System.in.read();

list1 = null;
System.out.println(2);
System.in.read();
System.out.println("end...");
}
}
1
2
3
jps
# b->二进制 live->存活对象(live会在快照前主动进行 GC) file -> 将快照存储在1.bin 文件中
jamp -dump:format=b,live,file=1.bin 21384

通过 MAT 查看 GC_ROOTS

image-20230129210113990

Total: 4 entries

  • System Class
  • Native Stack
  • Thread
  • Busy Monitor
五种引用
image-20230129211027300
  1. 强引用
    • 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  2. 软引用(SoftReference)
    • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
    • 可以配合引用队列来释放软引用自身
  3. 弱引用(WeakReference)
    • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
    • 可以配合引用队列来释放弱引用自身
  4. 虚引用(PhantomReference) –关联引用队列
    • 必须配合引用队列使用,主要配合 ByteBuffer 使用被引用对象回收时,会将虚引用入队(主要其引用对象被回收,而直接内存并没有被回收),由 Reference Handler 线程调用虚引用相关方法(调用Unsafe.freeMemory释放直接内存
  5. 终结器引用(FinalReference) –关联引用队列
    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程一个优先级很低的线程)通过终结器引用找到被引用对象并调用它的 finalize 方法( Object类中有一个 finalize()方法当重写了 finalize()方法 –希望在 当前对象被GC时调用 并没有强引用引用其时,就可以被GC回收,回收时,进入终结器引用队列,通过 Finalizer线程扫描引用队列并执行其 finalize()方法后才会被回收 ),第二次 GC 时才能回收被引用对象

不推荐使用 finalize() 释放内存

软引用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 演示软引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_3 {

private static final int _4MB = 4 * 1024 * 1024;

public static void main(String[] args) throws IOException {
/*List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();*/
soft();
}

public static void soft() {
// list --> SoftReference --> byte[]

List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}

软引用配合引用队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 演示软引用, 配合引用队列
*/
public class Demo2_4 {
private static final int _4MB = 4 * 1024 * 1024;

public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();

// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

for (int i = 0; i < 5; i++) {
// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}

// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}

System.out.println("===========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
}

弱引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_5 {
private static final int _4MB = 4 * 1024 * 1024;

public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
}

软引用与弱引用都是避免强引用导致 OOM 问题

  • 软引用 -> list –> SoftReference –> byte[]
  • 弱引用 -> list –> WeakReference –> byte[]

垃圾回收算法

常见的三种回收算法

  1. 标记清除法
  2. 标记整理法
  3. 复制回收法

标记清除法

定义: Mark Sweep

标记清除法主要分为两个阶段

沿着 GCROOT 寻找,==一阶段 -> 标记哪些没有引用的对象,将没有引用的对象标记为垃圾==。==二阶段 -> 再对标记的垃圾对象进行回收,回收也并不是将内存直接释放,而是将所回收对象占用内存起始地址结束地址存储在空闲内存列表中==.当再次分配此段内存时,进行覆盖

特点:

  • 速度较快
  • 会造成内存碎片
image-20230129213846780

标记整理法

定义:Mark Compact

特点:

  • 速度较慢
  • 没有内存碎片
image-20230129213944215

复制回收法

定义:Copy

特点:

  • 不会有内存碎片
  • 需要占用双倍内存空间
image-20230129214043107

分代垃圾回收

分代垃圾回收概念

image-20230129215758960

新生代处理生命周期短的对象,老年代处理生命周期长的对象 –针对不同区域采用不同的垃圾回收方法(垃圾分类)

==工作原理:== –空间不足时才会触发 GC

  • 对象创建后首先分配在伊甸园区域 -> 伊甸园区域内存不足时,就会触发 minor gc -> copy(通过幸存区 From To 实现) 存活的对象到 幸存区 ,并使其 年龄+1
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world(minor gc触发 STW 的时间很小),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,**会晋升至老年代**,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

进入老年代的方式

  1. 占用内存较大的对象,直接进入老年代

    • 程序一般认为大对象有可能是需要存活比较长时间的对象,而且大对象在幸存区返回来回复制,影响young GC的效率,
    • 通过-XX:PretenureSizeThreshold进行设置
  2. 年龄达到阈值

    • 通过-XX:MaxTenuringThreshold进行设置,默认为15
  3. 动态年龄判断

    • 当一个对象从Eden区到了Survivor区,当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而不需要达到默认的分代年龄
  4. 幸存区的内存不足

    1
    2
    3
    -XX:NewSize=10485760 -XX:MaxNewSize=10485760 -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=5242880 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log

    # 新生代 10M 、伊甸区 8M、幸存区1 1M、幸存区2 1M、堆 20M、老年代 10M、年龄阈值是15、大对象是 10M;使用ParNewGC+CMS;打印相关gc日志到gc.log文件
    • 在幸存区内存不足时,会将部分对象放置到幸存区,部分对象放入老年代

相关 JVM 参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

GC案例

  • 触发一次 MinorGC
image-20230129224550527
  • 将 eden space 占用提升到 98%
image-20230129224718852
  • 触发两次 MinorGC
image-20230129224838168
  • Big Object直接进入老年代
image-20230129225024054
  • 触发 OOM
image-20230129225121508
  • 虽然内存是线程共享,当前线程触发 OOM ,并不会影响已经在运行的线程(or 主线程),除非其他线程再次需要开辟空间(or 引发OOM)
image-20230129225217701

垃圾回收器

串行

特点

  • 单线程
  • 堆内存较小,适合个人电脑

参数

  • -XX:+UseSerialGC = Serial + SerialOld
    • Serial 工作在新生代
    • SerialOld 工作在老年代
image-20230129231349686

吞吐量优先

特点

  • 多线程 –并行
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高

参数

  • -XX:+UseParallelGC ~ -XX:+UseParallelOldGC –jdk1.8默认开启
  • -XX:+UseAdaptiveSizePolicy –自适应,调整各区域大小
  • -XX:GCTimeRatio=ratio –调整GC与总时间占比
  • -XX:MaxGCPauseMillis=ms –默认200ms 最大 GC STW 暂停时间
  • -XX:ParallelGCThreads=n –控制CPU线程数
image-20230129232108951

CPU占用趋势

image-20230129232715976

响应时间优先

特点

  • 多线程 –并行
  • 堆内存较大,多核 cpu
  • 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

==CMSGC==

参数

  • -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld –用户线程与GC线程同时执行,同时争抢CPU时间片,开启回收器
  • -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads –4 -> 1 n -> n/4
  • -XX:CMSInitiatingOccupancyFraction=percent –执行CMS内存占比 -> 预留空间给浮动垃圾
  • -XX:+CMSScavengeBeforeRemark –重新标记前,是否执行回收

CMS基于标记清除算法,有可能会造成大量 碎片内存 -> 从而引发并发失败,此时CMSGC就不能正常工作,导致退化成 SerialOldGC(串行整理 -> STW 时间特别长)

image-20230129232241335

CPU占用趋势

image-20230129233803355

G1

G1概念及原理

G1概念

Garbage First –Garbage One(一款很有年代的**垃圾回收器**,直到17年左右技术才相对成熟)

  • 2004 论文发布
  • 2009 JDK 6u14 体验
  • 2012 JDK 7u4 官方支持
  • 2017 JDK 9 纳为默认GC回收器 -> 从而取代CMS回收器

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region 每一个区都可以独立作为 Eden / Survive / Old (化整为零思想)
  • 整体上 单个Region标记+整理 算法,两个区域之间是 复制 算法

相关参数

  • -XX:+UseG1GC –显示开启使用 G1 -> jdk1.9 后默认开启
  • -XX:G1HeapRegionSize=size –设置 Region 大小 -> 最好/必须 设置成 2^n
  • -XX:MaxGCPauseMillis=time
G1 垃圾回收阶段
image-20230130000021564

阶段一

  • Young Collection –新生代收集
    • 会导致 STW –相对较短
image-20230130000148827

复制回收 copy -> survive

image-20230130000221800

survive age -> threshold -> Old / surive

image-20230130000445008

阶段二

  • Young Collection + CM

    • 在 Young GC 时会进行 GC Root 的初始标记

    • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

      -XX:InitiatingHeapOccupancyPercent=percent –(默认45%)

mark

image-20230130000724967

阶段三

  • Mixed Collection –会对 E、S、O 进行全面垃圾回收

    • 最终标记(Remark)会 STW

    • 拷贝存活(Evacuation)会 STW

      -XX:MaxGCPauseMillis=ms –控制 MaxGC 的 STW 时间。G1 会挑出回收价值高的 Old 来进行回收,从而来达到 MaxGCPauseMillis 的限制

image-20230130001005885
Full GC 辨析
  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足 -分两种情况
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足 -分两种情况 -> threshold >= 45% -> 触发标记-清除-并发清理(不是 FullGC) -> 在 remark 期间判断当 垃圾产生速度 > GC 速度时 -> 触发 FullGC
Young Collection 跨代引用问题

为防止 search old 的时候开销太大,所以老年代存储使用一种卡表技术,老年代 region 细分化为 cards -> 每个card 大概 512k

  • 新生代回收的跨代引用(老年代引用新生代)问题
image-20230130002349990

==如果老年代有 card 引用了新生代的对象,就将此 card 标记为 脏卡== –从而减少 search old 开销

同时新生代也会使用 Remembered Set 来记录有哪些 old card 引用了这些新生代对象,达到减少 search 开销的目的

  • 卡表与 Remembered Set
  • 在引用变更(脏卡)时通过 post-write barrier(写屏障) + dirty card queue (脏卡队列)
  • concurrent refinement threads 更新 Remembered Set
image-20230130002726856
Remark 重标记

对 E、S、O 进行全面垃圾回收时,会有两个阶段情况,先进行 remark -> 判断是否需要 FullGC

原理

  • pre-write barrier + satb_mark_queue
image-20230130003632501
  • 黑色 -> 已完成 remark 处理
  • 灰色 -> 正在进行 remark / 未完成(待完成) remark
  • 白色 -> 未进行 remark

问题:由于并发执行,如果在 remark 期间,有用户改变了对象之间的引用关系,就会导致,需要回收的对象没有被回收,不能回收的对象,被意外回收

–通过 pre-write barrier(写屏障) + satb_mark_queue(mark队列)来解决问题

image-20230130004007273

在发生引用改变时,配合 写屏障 将引用改变的对象放入 markqueue,最后统一再进行一次 remark 处理 markqueque 中的对象

不同 jdk G1 优化

主要围绕 jdk 8 9

  • JDK 8u20 字符串去重
    • 优点:节省大量内存
    • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

-XX:+UseStringDeduplication

1
2
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列

  • 新生代回收时,G1并发检查是否有字符串重复

  • 如果它们值一样,让它们引用同一个 char[]

  • 注意,与 String.intern() 不一样

    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是 char[]
    • 在 JVM 内部,使用了不同的字符串表 –与 串池 对比使用了不同的字符串表
  • JDK 8u40 并发标记类卸载

    所有对象都经过并发标记后,就能知道哪些类(包括实例)不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

    -XX:+ClassUnloadingWithConcurrentMark 默认启用

  • JDK 8u60 回收巨型对象

    • 一个对象大于 region 的一半时,称之为巨型对象 –G1会有巨型对象区
    • G1 不会对巨型对象进行拷贝
    • 回收时被优先考虑
    • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉(尽早回收)
image-20230130013106452
  • JDK 9 并发标记起始时间的调整

    • 并发标记通过提前开始并发标记来优化 FullGC)必须在堆空间占满完成,否则退化为 FullGC (虽然 FullGC 被优化为多线程,还是需要尽量减少)
    • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
    • JDK 9 可以动态调整
      • -XX:InitiatingHeapOccupancyPercent 用来设置初始值 例如 45%
      • 进行数据采样并动态调整
      • 总会添加一个安全的空档空间
  • JDK 9 更高效的回收

垃圾回收调优

预备知识

  • GC 相关的 VM 参数,会基本的空间调整
  • 相关工具
  • 切记调优跟应用、环境有关,没有放之四海而皆准的法则 –根据实际情况

主要是经验活

1
2
3
4
5
6
/*
查看虚拟机运行参数
"C:\Program Files\Java\jdk1.8.0_91\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
*/
public class Demo2_8 {
}

调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io

调优目标

  • 目的是 -> 【低延迟 –互联网项目,追求用户体验】还是【高吞吐量 –科学运算,追求高吞吐量,计算能力】,选择合适的回收器
  • CMS,G1,ZGC超低延迟 –响应时间优先
  • ParallelGC –高吞吐
  • Zing VM –零停顿

最快 GC

最快 GC 是不发生 GC

  • 查看 FullGC 前后的内存占用,考虑下面几个问题
    1. 数据是不是太多?
      • resultSet = statement.executeQuery(“select * from 大表 limit n“)
    2. 数据表示是否太臃肿?
      • 对象图,用到什么查什么
      • 对象大小 最小Object 16byte Integer(包装头 16byte) 24byte int 4byte
    3. 是否存在内存泄漏?
      • static Map map = –> 不释放?
      • 软引用
      • 弱引用
      • 第三方缓存实现

新生代调优

新生代优化空间更大

  • 新生代的特点
    • 所有的 new 操作的内存分配非常廉价 在 Eden 中
      • TLAB thread-local allocation buffer –new新对象会先判断 TLAB 缓存中是否存在,而 TLAB 的作用除了最为缓存减少new对象开销,还减少了在多线程new对象时的冲突问题(缓存解决冲突原理
    • 死亡对象的回收代价是零 –被copy的对象,直接覆盖
    • 大部分对象用过即死
    • Minor GC 的时间远远低于 Full GC
  • 越大越好吗?
    • 新生代中复制时间 > 标记时间 -> 而新生代通常最终只会有少量的对象存活下来,调大 size 并不会明显减少时间开销,所以并不是越大越好

-Xmn

Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.

  • 新生代能容纳所有【并发量 * (请求-响应的内存)】的数据 –合理值 -> 同一时刻 1000 qps 请求响应占用内存为 512k -> 大约 512m
  • 幸存区大到能保留【当前活跃对象+需要晋升对象】
  • 晋升阈值配置得当,让存活时间短的对象保存在幸存区,让长时间存活对象尽快晋升

-XX:MaxTenuringThreshold=threshold

-XX:+PrintTenuringDistribution –显示详细信息

1
2
3
4
5
Desired survivor size 48286924 bytes, new threshold 10 (max 10)
- age 1: 28992024 bytes, 28992024 total
- age 2: 1366864 bytes, 30358888 total
- age 3: 1425912 bytes, 31784800 total
...

新生代Size与吞吐量关系

image-20230130020617028

老年代调优

以 CMS 为例

  • CMS 的老年代内存越大越好 –CMS并发GC,浮动垃圾,并发失败,SerialOld
  • 尝试不做调优,如果没有 Full GC 那么已经不是 Old 性能卡点…,否则 先尝试调优新生代
  • 观察发生 Full GC 时,性能卡点在老年代内存占用,将原有老年代内存预设调大 1/4 ~ 1/3,从而减少 FullGC 次数来降低老年代的 GC 开销
    • -XX:CMSInitiatingOccupancyFraction=percent

较为极端值 -> 将 CMSInitiatingOccupancyFraction 设为 0,只要 Old 产生垃圾就进行回收,这样就不会导致浮动垃圾过多,引发并发失败。缺点是对于 CPU 要求较高,必须控制一定存在一个 CPU 线程,一直处理 OdlGC

常规控制在 70% - 80% -> 提供浮动垃圾的内存

案例

  1. 案例1 Full GC 和 Minor GC频繁

    • 新生代 -> 增大内存减少 minor gc
    • 增加晋升阈值 -> 使生命周期较短的对象尽量留在 survive -> 从而减少 Full GC
  2. 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)

    • 业务需求 -> 低延迟
    • 分析 CMS 哪个阶段耗时较长 -> CMS通常 重新标记阶段开销较大 -> 通过在重新标记之前,进行一次 minor gc 来减少重新标记的开销 -> -XX:+CMSScavengeBeforeRemark -> 减少单次 Full GC开销
  3. 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

    • 永久带原因? -> jdk1.8之前,永久带的空间不足,同样会触发 Full GC -> 增加元空间的初始值与最大值,来解决永久带空间不足问题

4. 字节码

image-20230130023122098

Java源代码,编译成 Java Class字节码类文件(学习class字节码类文件结构 -> 包含字节码指令,与Java source编译成 class 文件过程中的优化处理)。编译完成后,Java Class字节码类文件需要经过类加载器ClassLoader类加载的各个阶段,有哪些类加载器)进行类加载。类加载器将字节码加载到 VM 中,由 VM 执行其中的字节码指令,需要由执行引擎中的 Interpreter(解释器)来进行解释执行,在解释过程中,会对热点代码进行运行期的编译处理(JavaVM 是通过解释和编译来运行的 -> 需要了解运行期间的优化有一定理解)

Java Source -> javac -> 0x字节码与字节码指令 -> 解释(-> Interpreter解释器,部分查表,部分解释成机器码指令) -> 运行时进行部分及时编译(JIT)

类文件结构

分析类文件结构

HelloWorld.java

1
2
3
4
5
6
// HelloWorld 示例
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}

执行 javac -parameters -d . HellowWorld.java

-parameters -d 编译后保留方法中参数的名称信息

编译为 HelloWorld.class 后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[root@localhost ~]# od -t xC HelloWorld.class
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14

根据 JVM 规范,类文件结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic; // 前4字节
u2 minor_version; // 接下来2字节
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags; // 访问修饰
u2 this_class; //类名 包名
u2 super_class; // 父类
u2 interfaces_count; // 接口信息
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count; // 类附加的属性信息
attribute_info attributes[attributes_count];
}

魔数

Magic Number -> A constant numerical or text value used to identify a file format。

魔数 -> 标识文件的格式使用魔数,而不是扩展名。主要是基于安全方面的考虑,因为文件的扩展名是可以随意修改的,例如 PDF文件魔数使用的文本值”%PDF”(ASCII字符),底层编码十六进制形式(25 50 44 46);GIF文件的魔数使用的文本值”GIF89a”(ASCII字符),底层编码十六进制形式 (47 49 46 38 39 61) 。Java的Class文件魔数使用数值“CAFEBABE”(十六进制),底层编码就是该数值,.java文件魔数使用数值”CAFEDEAD”
参考文档

参考文档

0~3 字节,表示它是否是【class】类型的文件

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

版本

4~7 字节,表示类的版本 00 34(52) 表示是 Java 8 –指体现了 jdk8 大版本,没有体现小版本

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

51 jdk7

52 jdk8

53 jdk9

常量池

常量池占用整个 class 文件中较大的比重

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18
  • ==89 字节,表示常量池长度(整个常量池中有多少项),00 23 (35) 表示常量池有 #1#34项,注意 #0 项不计入,也没有值==

    0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  1. 第#1项 0a 表示一个 Method(查常量池表 -> 表示一个方法引用信息) 信息(常量的类型信息),00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

  1. 第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项来获得这个成员变量的【所属类】和【成员变量名】

0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  1. 第#3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  1. 第#4项 0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26 项来获得这个方法的【所属类】和【方法名】

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  1. 第#5项 07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

  1. 第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项

0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

  1. 第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【 <init> -> 表示类的构造方法

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

  1. 第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【()V】其实就是表示无参、无返回值

0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

  1. 第#9项 01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65 是【Code】

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

  1. 第#10项 01 表示一个 utf8 串,00 0f(15) 表示长度,4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 是【LineNumberTable】

0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e

0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63

  1. 第#11项 01 表示一个 utf8 串,00 12(18) 表示长度,4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65是【LocalVariableTable】

0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63

0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01

  1. 第#12项 01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是【this】

0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01

0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63

  1. 第#13项 01 表示一个 utf8 串,00 1d(29) 表示长度,是【Lcn/itcast/jvm/t5/HelloWorld;】-> 引用类型

0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63

0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f

0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16

  1. 第#14项 01 表示一个 utf8 串,00 04 表示长度,74 68 69 73 是【main

0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16

  1. 第#15项 01 表示一个 utf8 串,00 16(22) 表示长度,是【([Ljava/lang/String;)V】-> 参数类型和返回类型,其实就是参数为字符串数组,无返回值

0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16

0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72

0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13

  1. 第#16项 01 表示一个 utf8 串,00 04 表示长度,是【args】-> 参数名称

0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13

  1. 第#17项 01 表示一个 utf8 串,00 13(19) 表示长度,是【[Ljava/lang/String;】-> 参数类型

0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13

0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69

0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61

  1. 第#18项 01 表示一个 utf8 串,00 10(16) 表示长度,是【MethodParameters】-> 方法参数信息

0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61

0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46

  1. 第#19项 01 表示一个 utf8 串,00 0a(10) 表示长度,是【SourceFile】-> 类属性信息,源文件

0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46

0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64

  1. 第#20项 01 表示一个 utf8 串,00 0f(15) 表示长度,是【HelloWorld.java】

0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

  1. 第#21项 0c 表示一个 【名+类型】,00 07 00 08 引用了常量池中 #7 #8 两项

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

  1. 第#22项 07 表示一个 Class 信息,00 1d(29) 引用了常量池中 #29 项

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

  1. 第#23项 0c 表示一个 【名+类型】,00 1e(30) 00 1f (31)引用了常量池中 #30 #31 两项

0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e

0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64

  1. 第#24项 01 表示一个 utf8 串,00 0f(15) 表示长度,是【hello world】

0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64

  1. 第#25项 07 表示一个 Class 信息,00 20(32) 引用了常量池中 #32 项

0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74

  1. 第#26项 0c 表示一个 【名+类型】,00 21(33) 00 22(34)引用了常量池中 #33 #34 两项

0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74

  1. 第#27项 01 表示一个 utf8 串,00 1b(27) 表示长度,是【cn/itcast/jvm/t5/HelloWorld】

0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74

0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c

0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61

  1. 第#28项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/Object】

0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61

0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61

  1. 第#29项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/System】

0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61

0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f

  1. 第#30项 01 表示一个 utf8 串,00 03 表示长度,是【out】

0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f

0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72

  1. 第#31项 01 表示一个 utf8 串,00 15(21) 表示长度,是【Ljava/io/PrintStream;】

0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72

0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76

  1. 第#32项 01 表示一个 utf8 串,00 13(19) 表示长度,是【java/io/PrintStream】

0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76

0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d

  1. 第#33项 01 表示一个 utf8 串,00 07 表示长度,是【println】

0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a

  1. 第#34项 01 表示一个 utf8 串,00 15(21) 表示长度,是【(Ljava/lang/String;)V】

0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a

0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

访问标识与继承信息

21 表示该 class 是一个类,公共的 20 + 1 -> 公共类

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

05 表示根据常量池中 #5 找到本类全限定名

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

06 表示根据常量池中 #6 找到父类全限定名

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

表示接口的数量,本类为 0

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public ; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final ; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction. –表示类或者超类特殊方法
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract ; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code. –人工合成的,不是源代码的
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

Field 信息

表示成员变量数量,本类为 0

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
FieldType Type Interpretation
B byte signed byte
C char Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L
ClassName
;
reference an instance of class ClassName
S short signed short
Z boolean true or false
[ reference one array dimension

Method 信息

表示方法数量,本类为 2

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成

  • 红色代表访问修饰符(本类中是 public)
  • 蓝色代表引用了常量池 #07 项作为方法名称
  • 绿色代表引用了常量池 #08 项作为方法参数描述
  • 黄色代表方法属性数量,本方法是 1
  • 红色代表方法属性
    • 00 09 表示引用了常量池 #09 项,发现是【Code】属性
    • 00 00 00 2f 表示此属性的长度是 47
    • 00 01 表示【操作数栈】最大深度
    • 00 01 表示【局部变量表】最大槽(slot)数
    • 00 00 00 05 表示字节码长度,本例是 5
    • 2a b7 00 01 b1 是字节码指令
    • 00 00 00 02 表示方法细节属性数量,本例是 2
    • 00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
      • 00 00 00 06 表示此属性的总长度,本例是 6
      • 00 01 表示【LineNumberTable】长度
      • 00 00 表示【字节码】行号 00 04 表示【java 源码】行号
    • 00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
      • 00 00 00 0c 表示此属性的总长度,本例是 12
      • 00 01 表示【LocalVariableTable】长度
      • 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
      • 00 05 表示局部变量覆盖的范围长度
      • 00 0c 表示局部变量名称,本例引用了常量池 #12 项,是【this】
      • 00 0d 表示局部变量的类型,本例引用了常量池 #13 项,是 【Lcn/itcast/jvm/t5/HelloWorld;】
      • 00 00 表示局部变量占有的槽位(slot)编号,本例是 0

0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01

0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00

0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00

0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00

  • 红色代表访问修饰符(本类中是 public 01 static 08)
  • 蓝色代表引用了常量池 #14 项作为方法名称
  • 绿色代表引用了常量池 #15 项作为方法参数描述
  • 黄色代表方法属性数量,本方法是 2
  • 红色代表方法属性(属性1)
    • 00 09 表示引用了常量池 #09 项,发现是【Code】属性
    • 00 00 00 37 表示此属性的长度是 55
    • 00 02 表示【操作数栈】最大深度
    • 00 01 表示【局部变量表】最大槽(slot)数
    • 00 00 00 05 表示字节码长度,本例是 9
    • b2 00 02 12 03 b6 00 04 b1 是字节码指令
    • 00 00 00 02 表示方法细节属性数量,本例是 2
    • 00 0a 表示引用了常量池 #10 项,发现是【LineNumberTable】属性
      • 00 00 00 0a 表示此属性的总长度,本例是 10
      • 00 02 表示【LineNumberTable】长度
      • 00 00 表示【字节码】行号 00 06 表示【java 源码】行号
      • 00 08 表示【字节码】行号 00 07 表示【java 源码】行号
    • 00 0b 表示引用了常量池 #11 项,发现是【LocalVariableTable】属性
      • 00 00 00 0c 表示此属性的总长度,本例是 12
      • 00 01 表示【LocalVariableTable】长度
      • 00 00 表示局部变量生命周期开始,相对于字节码的偏移量
      • 00 09 表示局部变量覆盖的范围长度
      • 00 10 表示局部变量名称,本例引用了常量池 #16 项,是【args】
      • 00 11 表示局部变量的类型,本例引用了常量池 #17 项,是【[Ljava/lang/String;】
      • 00 00 表示局部变量占有的槽位(slot)编号,本例是 0

0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00

0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00

0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a

0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b

0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00

红色代表方法属性(属性2)

  • 00 12 表示引用了常量池 #18 项,发现是【MethodParameters】属性
    • 00 00 00 05 表示此属性的总长度,本例是 5
    • 01 参数数量
    • 00 10 表示引用了常量池 #16 项,是【args】
    • 00 00 访问修饰符

0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00

0001120 00 00 02 00 14

附加属性

  • 00 01 表示附加属性数量
  • 00 13 表示引用了常量池 #19 项,即【SourceFile】
  • 00 00 00 02 表示此属性的长度
  • 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】

0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00

0001120 00 00 02 00 14

参考文献 -Java虚拟机规范

字节码指令

字节码指令理论

接着上一节,研究一下两组字节码指令,一个是

public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令

1
2a b7 00 01 b1

==jvm通过 解释器(Interpreter)来识别以上VM平台无关字节码指令,将其解释成机器码,然后直接执行机器码== -> ==字节码指令可以通过查看官方文档,解释成 **操作码(助记符)**,使其便于阅读==

  1. 2a => aload_0(注记符->便于我们查看) 加载 slot 0 的局部变量(**-> 到操作数栈上,JVM解释器执行时需要到操作数栈上读取数据**),即 this,做为下面的 invokespecial 构造方法调用的参数
  2. b7 => invokespecial 预备调用构造方法,哪个方法呢?
  3. 00 01 引用常量池中 #1 项,即【 Method java/lang/Object."":()V
  4. b1 表示返回 return

另一个是 public static void main(java.lang.String[]); 主方法的字节码指令

1
b2 00 02 12 03 b6 00 04 b1
  1. b2 => getstatic 用来加载静态变量(加载到操作数栈上),哪个静态变量呢?
  2. 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
  3. 12 => ldc 加载参数,哪个参数呢?
  4. 03 引用常量池中 #3 项,即 【String hello world】
  5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?
  6. 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
  7. b1 表示返回 return
image-20230131015553127

参考文档

javap 工具

由于分析类文件结构过于麻烦,Oracle 提供了 javap 工具来反编译 class 文件

javap -v HelloWorld.class -> -v 表示输出详细信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
[root@localhost ~]# javap -v HelloWorld.class
Classfile /root/HelloWorld.class
Last modified Jul 7, 2019; size 597 bytes
MD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbc
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #22.#23 //
java/lang/System.out:Ljava/io/PrintStream;
#3 = String #24 // hello world
#4 = Methodref #25.#26 // java/io/PrintStream.println:
(Ljava/lang/String;)V
#5 = Class #27 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #28 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 MethodParameters
#19 = Utf8 SourceFile
#20 = Utf8 HelloWorld.java
#21 = NameAndType #7:#8 // "<init>":()V
#22 = Class #29 // java/lang/System
#23 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#24 = Utf8 hello world
#25 = Class #32 // java/io/PrintStream
#26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
#27 = Utf8 cn/itcast/jvm/t5/HelloWorld
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/System
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Utf8 java/io/PrintStream
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/String;)V

{
public cn.itcast.jvm.t5.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object." <init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field
java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method
java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
}

javap 反编译工具来帮助我们简化阅读 class 字节码文件 -> 更清晰阅读字节码文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
E:\Todo\note\Software\Java_Note\JVM_Note>javap -v HelloWorld.class
Classfile /E:/Todo/note/Software/Java_Note/JVM_Note/HelloWorld.class
Last modified 2023-1-30; size 432 bytes
MD5 checksum d3dace27df9ccf67c8b58b911541fa63
Compiled from "HelloWorld.java"
public class Test02.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // hello world
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Test02/HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Test02/HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public Test02.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
SourceFile: "HelloWorld.java"

图解方法执行流程

java 代码

1
2
3
4
5
6
7
8
9
10
11
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}

编译后字节码文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
[root@localhost ~]# javap -v Demo3_1.class
Classfile /root/Demo3_1.class
Last modified Jul 7, 2019; size 665 bytes
MD5 checksum a2c29a22421e218d4924d31e6990cfc5
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #28.#29 //
java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#6 = Class #32 // cn/itcast/jvm/t3/bytecode/Demo3_1
#7 = Class #33 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcn/itcast/jvm/t3/bytecode/Demo3_1;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 MethodParameters
#24 = Utf8 SourceFile
#25 = Utf8 Demo3_1.java
#26 = NameAndType #8:#9 // "<init>":()V
#27 = Utf8 java/lang/Short
#28 = Class #34 // java/lang/System
#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(I)V
#32 = Utf8 cn/itcast/jvm/t3/bytecode/Demo3_1
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (I)V
{
public cn.itcast.jvm.t3.bytecode.Demo3_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field
java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method
java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 10
line 12: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
MethodParameters:
Name Flags
args
}
  1. 常量池载入运行时常量池
image-20230130102433463

注意 小数字并不是存储在常量池中而是与字节码指令存储在一起 -> 如 10 -> 而一旦数字超过了short整数的最大值 Short.MAX_VALUE+1,就会被存储在常量池中

  1. 方法字节码载入方法区
image-20230130102503672
  1. main 线程开始运行,分配栈帧内存

(stack=2,locals=4)

image-20230130102533975

栈帧又分为,局部变量表(locals=4)与操作数栈(stack=2 存储数据与字节码指令)

  1. 执行引擎开始执行字节码

bipush 10

  • 将一个 byte 压入操作数栈(操作数栈的宽度单位使,4字节,故而将其长度会补齐 4 个字节),类似的指令还有
  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
  • ldc 将一个 int(常量池中数据) 压入操作数栈
  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
image-20230130102627560

istore_1

  • 将操作数栈顶数据弹出,存入局部变量表的 slot 1 -> 槽位
image-20230130102655779 image-20230130102711519

ldc #3

  • 从常量池加载 #3 数据到操作数栈
  • 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
image-20230130102747310

istore_2

image-20230130102815074 image-20230130102833858

iload_1

image-20230130102855048

iload_2

image-20230130102915850

iadd

image-20230130102940090 image-20230130102951366

istore_3

image-20230130103050009 image-20230130103104858

getstatic #4

image-20230130103124540 image-20230130103137016

iload_3

image-20230130103205056 image-20230130103219333

invokevirtual #5

  • 找到常量池 #5 项
  • 定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 生成新的栈帧(分配 locals、stack等)
  • 传递参数,执行新栈帧中的字节码
image-20230130103308678
  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容
image-20230130103334036

return

  • 完成 main 方法调用,弹出 main 栈帧
  • 程序结束

练习 - 分析 i++

暂时可以有几个局部变量就几个槽位

目的:从字节码角度分析 a++ 相关题目

源码:

1
2
3
4
5
6
7
8
9
10
11
/**
* 从字节码角度分析 a++ 相关题目
*/
public class Demo3_2 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}

字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
18: getstatic #2 // Field
java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method
java/io/PrintStream.println:(I)V
25: getstatic #2 // Field
java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method
java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 18
line 11: 25
line 12: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I

分析:

  • ==注意 iinc 指令是直接在局部变量 slot 上进行运算==
  • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc
image-20230130103512529 image-20230130103536823 image-20230130103549930 image-20230130103614656 image-20230130103629149 image-20230130103639878 image-20230130103650675 image-20230130103701865 image-20230130103715500 image-20230130103726645 image-20230130103741012

条件判断指令

指令 助记符 含义
0x99 ifeq 判断是否 == 0
0x9a ifne 判断是否 != 0
0x9b iflt 判断是否 < 0
0x9c ifge 判断是否 >= 0
0x9d ifgt 判断是否 > 0
0x9e ifle 判断是否 <= 0
0x9f if_icmpeq 两个int是否 ==
0xa0 if_icmpne 两个int是否 !=
0xa1 if_icmplt 两个int是否 <
0xa2 if_icmpge 两个int是否 >=
0xa3 if_icmpgt 两个int是否 >
0xa4 if_icmple 两个int是否 <=
0xa5 if_acmpeq 两个引用是否 ==
0xa6 if_acmpne 两个引用是否 !=
0xc6 ifnull 判断是否 == null
0xc7 ifnonnull 判断是否 != null

说明:

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码

long,double,float怎么比较见 Java虚拟机指令表

源码:

1
2
3
4
5
6
7
8
9
10
public class Demo3_3 {
public static void main(String[] args) {
int a = 0;
if(a == 0) {
a = 10;
} else {
a = 20;
}
}
}

字节码:

1
2
3
4
5
6
7
8
9
10
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return

思考

以上比较指令中没有 long,float,double 的比较,那么它们要比较怎处理

参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp

循环控制指令

循环控制同样是前面介绍的那些指令

例如 while 循环:

1
2
3
4
5
6
7
8
public class Demo3_4 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}

字节码是:

1
2
3
4
5
6
7
8
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

比如 do while 循环:

1
2
3
4
5
6
7
8
public class Demo3_5 {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}

字节码是:

1
2
3
4
5
6
7
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 10
8: if_icmplt 2
11: return

最后再看 for 循环:

1
2
3
4
5
6
public class Demo3_6 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
}
}
}

字节码是:

1
2
3
4
5
6
7
8
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

注意

比较 while 和 for 的字节码,你发现它们是一模一样的,殊途同归

练习 - 判断结果

请从字节码角度分析,下列代码运行的结果:

1
2
3
4
5
6
7
8
9
10
11
public class Demo3_6_1 {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x); // 结果是 0
}
}
image-20230131024938366

构造方法

<cinit>()V
1
2
3
4
5
6
7
8
9
public class Demo3_8_1 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}

编译器会按从上至下的顺序,收集所有 static 静态代码块静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V

1
2
3
4
5
6
7
0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return

<cinit>()V 方法会在类加载的初始化阶段被调用

练习

自己调整一下 static 变量和静态代码块的位置,观察字节码的改动

<init>()V
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo3_8_2 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo3_8_2(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo3_8_2 d = new Demo3_8_2("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}

编译器会按从上至下的顺序,收集所有 {} 代码块成员变量赋值的代码形成新的构造方法,但原始构造方法内的代码总是在最后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public cn.itcast.jvm.t3.bytecode.Demo3_8_2(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0 // 加载 this
1: invokespecial #1 // super.<init>()V
4: aload_0
5: ldc #2 // <- "s1"
7: putfield #3 // -> this.a
10: aload_0
11: bipush 20 // <- 20
13: putfield #4 // -> this.b
16: aload_0
17: bipush 10 // <- 10
19: putfield #4 // -> this.b
22: aload_0
23: ldc #5 // <- "s2"
25: putfield #3 // -> this.a
28: aload_0 // ------------------------------
29: aload_1 // <- slot 1(a) "s3" |
30: putfield #3 // -> this.a |
33: aload_0 |
34: iload_2 // <- slot 2(b) 30 |
35: putfield #4 // -> this.b --------------------
38: return
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_8_2;
0 39 1 a Ljava/lang/String;
0 39 2 b I
MethodParameters: ...

方法调用

不同的方法调用对应的字节码指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo3_9 {
public Demo3_9() { }
private void test1() { }
private final void test2() { }
public void test3() { }
public static void test4() { }
public static void main(String[] args) {
Demo3_9 d = new Demo3_9();
d.test1();
d.test2();
d.test3();
d.test4();
Demo3_9.test4();
}
}

字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9 --分配堆空间内存
3: dup // 将分配成功的对象引用放入操作数栈,dup复制操作数栈,配合 init 使用
4: invokespecial #3 // Method "<init>":()V --构造
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V --private
12: aload_1
13: invokespecial #5 // Method test2:()V --final
16: aload_1
17: invokevirtual #6 // Method test3:()V --public(可能被重写,所以invokevirtual动态绑定,需要在运行时确定)
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V --static
25: invokestatic #7 // Method test4:()V --static
28: return
  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
  • dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “”:()V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量
  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用 invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法
  • invokespecial 与 invokestatic 性能相对于 invokevirtual 要高出一截

多态原理

多态的原理从字节码指令角度看就是 invokevirtual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*/
public class Demo3_10 {
public static void test(Animal animal) {
animal.eat();
System.out.println(animal.toString());
}

public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}
}

abstract class Animal {
public abstract void eat();
@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}

class Dog extends Animal {
@Override
public void eat() {
System.out.println("啃骨头");
}
}

class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
}
  1. 运行代码

停在 System.in.read() 方法上,这时运行 jps 获取进程 id

  1. 运行 HSDB 工具

借助 HSDB 工具才能看到 VM 底层的内存状态与内存地址

进入 JDK 安装目录,执行

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面 attach 输入 进程 id

注意执行代码的时候需要加上参数,禁止指针压缩

  1. 查找某个对象

打开 Tools -> Find Object By Query

输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行

select + 类型别名 + from 对象类型 别名

image-20230130153415487
  1. 查看对象内存结构

点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针

但目前看不到它的实际地址

image-20230130153500801

对象在内存中的实际表示如上,类加载以后创建出来的对象底层的class结构 -> 实际就是 C++ Expression 结构 -> 结构里包含 对象头 16byte(锁标记 + 对象的类型指针,根据类型指针找到对象的class类) 和 对象里的成员变量

==实际Class类在内存中的表示==

image-20230131033208864
  1. 查看对象 Class 的内存地址

可以通过 Windows -> Console 进入命令行模式,执行

mem 0x00000001299b4978 2

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)

结果中第二行 0x000000001b7d4028 即为 Class 的内存地址

image-20230131033307710 image-20230130153611470

类class 的完成显示

image-20230131033355876
  1. 查看类的 vtable (类中的多态方法,都存储在一张 vtable 虚方法表中,而static final private 方法,都不会存储在虚方法表中)

虚方法表在具体类结构的最后 _default_vtable_indices vtable 与 class 的偏移地址为 1B8

方法1:Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面

image-20230130153640850

方法2:或者 Tools -> Class Browser 输入 Dog 查找,可以得到相同的结果

image-20230130153713226

无论通过哪种方法,都可以找到 Dog Class 的 vtable 长度为 6(_vtable_len: 6),意思就是 Dog 类有 6 个虚方法(多态相关的,final,static 不会列入)

那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:

1
2
3
4
0x000000001b7d4028
1b8 +
---------------------
0x000000001b7d41e0

通过 Windows -> Console 进入命令行模式,执行

1
2
3
4
5
6
7
8
mem 0x000000001b7d41e0 6

0x000000001b7d41e0: 0x000000001b3d1b10
0x000000001b7d41e8: 0x000000001b3d15e8
0x000000001b7d41f0: 0x000000001b7d35e8
0x000000001b7d41f8: 0x000000001b3d1540
0x000000001b7d4200: 0x000000001b3d1678
0x000000001b7d4208: 0x000000001b7d3fa8

就得到了 **6 个虚方法(virtual method)**的入口地址

  1. 验证方法地址

查看 Object animal Dog 直接按 虚方法()的关系

image-20230131034417149 image-20230131034453798

通过 Tools -> Class Browser 查看每个类的方法定义,比较可知

1
2
3
4
5
6
Dog - public void eat() @0x000000001b7d3fa8
Animal - public java.lang.String toString() @0x000000001b7d35e8;
Object - protected void finalize() @0x000000001b3d1b10;
Object - public boolean equals(java.lang.Object) @0x000000001b3d15e8;
Object - public native int hashCode() @0x000000001b3d1540;
Object - protected native java.lang.Object clone() @0x000000001b3d1678;

对号入座,发现

  • eat() 方法是 Dog 类自己的
  • toString() 方法是继承 String 类的
  • finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的
  1. 小结

    当执行 invokevirtual 指令时,

    1. 先通过栈帧中的对象引用找到对象
    2. 分析对象头,找到对象的实际 Class
    3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
    4. 查表得到方法的具体地址
    5. 执行方法的字节码

-> 多态的 invokevirtual 执行,涉及到运行时动态的查找,JVM也会对这种动态的JVM进行优化,缓存(反复调用同一个方法,就直接从缓存中调取方法),如果发现运行期间,多态并没有发挥作用,只调用了一个类型,就会做一个单态的优化

异常处理

try-catch
1
2
3
4
5
6
7
8
9
10
public class Demo3_11_1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}

注意

为了抓住重点,下面的字节码省略了不重要的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
StackMapTable: ...
MethodParameters: ...
}
  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
多个 single-catch 块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo3_11_2 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 30;
} catch (NullPointerException e) {
i = 40;
} catch (Exception e) {
i = 50;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 26
8: astore_2
9: bipush 30
11: istore_1
12: goto 26
15: astore_2
16: bipush 40
18: istore_1
19: goto 26
22: astore_2
23: bipush 50
25: istore_1
26: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/ArithmeticException;
16 3 2 e Ljava/lang/NullPointerException;
23 3 2 e Ljava/lang/Exception;
0 27 0 args [Ljava/lang/String;
2 25 1 i I
StackMapTable: ...
MethodParameters: ...
  • 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用没必要用三个 solt 槽位来存储异常对象引用,所以直接使用槽位复用
multi-catch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo3_11_3 {
public static void main(String[] args) {
try {
Method test = Demo3_11_3.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException |
InvocationTargetException e) {
e.printStackTrace();
}
}
public static void test() {
System.out.println("ok");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: ldc #2
2: ldc #3
4: iconst_0
5: anewarray #4
8: invokevirtual #5
11: astore_1
12: aload_1
13: aconst_null
14: iconst_0
15: anewarray #6
18: invokevirtual #7
21: pop
22: goto 30
25: astore_1
26: aload_1
27: invokevirtual #11 // e.printStackTrace:()V
30: return
Exception table:
from to target type
0 22 25 Class java/lang/NoSuchMethodException
0 22 25 Class java/lang/IllegalAccessException
0 22 25 Class java/lang/reflect/InvocationTargetException
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 10 1 test Ljava/lang/reflect/Method;
26 4 1 e Ljava/lang/ReflectiveOperationException;
0 31 0 args [Ljava/lang/String;
StackMapTable: ...
MethodParameters: ...
  • 区分异常类型分配槽位,三个不同的异常都存储在Exception table异常表中,相比于上述的异常,新语法并没有使字节码指令有什么变化
finally
1
2
3
4
5
6
7
8
9
10
11
12
public class Demo3_11_4 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 // try --------------------------------------
4: istore_1 // 10 -> i |
5: bipush 30 // finally |
7: istore_1 // 30 -> i |
8: goto 27 // return -----------------------------------
11: astore_2 // catch Exceptin -> e ----------------------
12: bipush 20 // |
14: istore_1 // 20 -> i |
15: bipush 30 // finally |
17: istore_1 // 30 -> i |
18: goto 27 // return -----------------------------------
21: astore_3 // catch any -> slot 3 ----------------------
22: bipush 30 // finally |
24: istore_1 // 30 -> i |
25: aload_3 // <- slot 3 |
26: athrow // throw ------------------------------------
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
StackMapTable: ...
MethodParameters: ...

可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程,从而保证,finally块中的内容一定会被执行

catch Exception 并不能捕获所有异常,有可能出现,Exception以外的(如 Exception 平级或父级的异常),这时又要保证 finally块中的代码一定被执行,就需要 any 来弥补剩余所有情况(即第三个分支)

练习 - finally 面试题

finally 中出现 return

下面的题目输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result); // 20
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> slot 0 (从栈顶移除了)
3: bipush 20 // <- 20 放入栈顶
5: ireturn // 返回栈顶 int(20)
6: astore_1 // catch any -> slot 1
7: bipush 20 // <- 20 放入栈顶
9: ireturn // 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
LineNumberTable: ...
StackMapTable: ...
  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常,可以试一下下面的代码 -> 即使用 finally 直接执行了 return 结束了函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo3_12_1 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
int i = 1/0;
return 10;
} finally {
return 20;
}
}
}
finally 对返回值影响

下面的题目输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result); // 10
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> i
3: iload_0 // <- i(10)
4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值
5: bipush 20 // <- 20 放入栈顶
7: istore_0 // 20 -> i
8: iload_1 // <- slot 1(10) 载入 slot 1 暂存的值
9: ireturn // 返回栈顶的 int(10)
10: astore_2
11: bipush 20
13: istore_0
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
StackMapTable: ...

2: istore_0 // 10 -> i 固定了返回值,最后会再次载入 10 后再 return –故而不会导致结果变化

synchronized

1
2
3
4
5
6
7
8
public class Demo3_13 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial <init>:()V
7: astore_1 // lock引用 -> lock
8: aload_1 // <- lock (synchronized开始)
9: dup // 提供给加锁和解锁使用
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter(lock引用)
12: getstatic #3 // <- System.out
15: ldc #4 // <- "ok"
17: invokevirtual #5 // invokevirtual println:
(Ljava/lang/String;)V
20: aload_2 // <- slot 2(lock引用)
21: monitorexit // monitorexit(lock引用)
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // <- slot 2(lock引用)
27: monitorexit // monitorexit(lock引用)
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: ...
MethodParameters: ...

确保解锁动作还是使用 Exception table 来验证同一个对象,再解锁。即使运行过程中出现了异常,也会通过 Exception table 来确保队同一个对象进行解锁操作

注意

方法级别的 synchronized 不会在字节码指令中有所体现

编译期处理

所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)

注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记

默认构造器

1
2
public class Candy1 {
}

编译成class后的代码

1
2
3
4
5
6
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}

自动拆装箱

这个特性是 JDK 5 开始加入的, 代码片段1

1
2
3
4
5
6
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}

这段代码在 JDK 5 之前是无法编译通过的,必须改写为 代码片段2 :

1
2
3
4
5
6
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}

显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2 -128 ~ 127 之间的 int -> 通过 .valueOf() / .intValue() -> 优化成编译期间自动

泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作(有些是不会被擦除的),即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

1
2
3
4
5
6
7
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
}
}

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:

1
2
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);

如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:

1
2
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

还好这些麻烦事都不用自己做。

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public cn.itcast.jvm.t3.candy.Candy3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/candy/Candy3;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList." <init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokeinterface #5, 2 // InterfaceMethodjava/util/List.add:(Ljava/lang/Object;)Z --> 不管你是 String Integer 泛型擦除统一视为 Object
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // InterfaceMethodjava/util/List.get:(I)Ljava/lang/Object;
27: checkcast #7 // class java/lang/Integer --> 将 Object 强制类型转换 检查构建类型
30: astore_2
31: return
LineNumberTable:
line 8: 0
line 9: 8
line 10: 20
line 11: 31
LocalVariableTable: // 局部变量类型表
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
8 24 1 list Ljava/util/List;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>; // 在局部变量类型表中,泛型信息任然被保留,不能通过反射直接获取,只能通过方法的参数和返回值上通过反射 get 泛型信息才能获取

使用反射,仍然能够获得这些信息:(泛型反射

1
2
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
1
2
3
4
5
6
7
8
9
10
11
12
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes(); // 得到泛型参数的类型信息
for (Type type : types) {
if (type instanceof ParameterizedType) { // 检查是否是泛型类型,判断不是原始类型 -> 转换成参数化类型
ParameterizedType parameterizedType = (ParameterizedType) type;
System.out.println("原始类型 - " + parameterizedType.getRawType()); // 原始类型
Type[] arguments = parameterizedType.getActualTypeArguments(); // 遍历转换
for (int i = 0; i < arguments.length; i++) {
System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}

输出

1
2
3
4
5
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

可变参数

可变参数也是 JDK 5 开始加入的新特性:

例如:

1
2
3
4
5
6
7
8
9
public class Candy4 {
public static void foo(String... args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello", "world");
}
}

可变参数 String... args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。

同样 java 编译器会在编译期间将上述代码变换为:==(Java编译器根据实参的数量,将可变参数转换成数组)==

1
2
3
4
5
6
7
8
9
public class Candy4 {
public static void foo(String[] args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}

注意

如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去

foreach 循环

仍是 JDK 5 开始引入的语法糖,数组的循环:

1
2
3
4
5
6
7
8
public class Candy5_1 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
for (int e : array) {
System.out.println(e);
}
}
}

会被编译器转换为:

1
2
3
4
5
6
7
8
9
10
11
public class Candy5_1 {
public Candy5_1() {
}
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 4, 5};
for(int i = 0; i < array.length; ++i) {
int e = array[i];
System.out.println(e);
}
}
}

而集合的循环:

1
2
3
4
5
6
7
8
public class Candy5_2 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
System.out.println(i);
}
}
}

实际被编译器转换为对迭代器的调用:

1
2
3
4
5
6
7
8
9
10
11
12
public class Candy5_2 {
public Candy5_2() {
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while(iter.hasNext()) {
Integer e = (Integer)iter.next();
System.out.println(e);
}
}
}

注意

foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator

switch 字符串

JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
}
}

注意

switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚

会被编译器转换为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Candy6_1 {
public Candy6_1() {
}
public static void choose(String str) {
byte x = -1;
switch(str.hashCode()) {
case 99162322: // hello 的 hashCode
if (str.equals("hello")) {
x = 0;
}
break;
case 113318802: // world 的 hashCode
if (str.equals("world")) {
x = 1;
}
}
switch(x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}

可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BMC. 这两个字符串的hashCode值都是 2123 ,如果有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Candy6_2 {
public static void choose(String str) {
switch (str) {
case "BM": {
System.out.println("h");
break;
}
case "C.": {
System.out.println("w");
break;
}
}
}
}

会被编译器转换为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Candy6_2 {
public Candy6_2() {
}
public static void choose(String str) {
byte x = -1;
switch(str.hashCode()) {
case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
if (str.equals("C.")) {
x = 1;
} else if (str.equals("BM")) {
x = 0;
}
default:
switch(x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
}

switch 枚举

switch 枚举的例子,原始代码:

1
2
3
enum Sex {
MALE, FEMALE
}
1
2
3
4
5
6
7
8
9
10
public class Candy7 {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男"); break;
case FEMALE:
System.out.println("女"); break;
}
}
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Candy7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见,静态内部类)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存储case用来对比的数字
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}

枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

1
2
3
enum Sex {
MALE, FEMALE
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public final class Sex extends Enum<Sex> {
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is
assigned
*/
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}

try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources

1
2
3
4
5
try(资源变量 = 创建资源对象){

} catch( ) {

}

其中资源对象需要实现 AutoCloseable 接口,例如 InputStreamOutputStreamConnectionStatementResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

1
2
3
4
5
6
7
8
9
public class Candy9 {
public static void main(String[] args) {
try(InputStream is = new FileInputStream("d:\\1.txt")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
}
}

会被转换为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Candy9 {
public Candy9() {
}
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\1.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
// t 是我们代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果我们代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 如果 close 出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test6 {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
int i = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable {
public void close() throws Exception {
throw new Exception("close 异常");
}
}

输出:

1
2
3
4
5
java.lang.ArithmeticException: / by zero
at test.Test6.main(Test6.java:7)
Suppressed: java.lang.Exception: close 异常
at test.MyResource.close(Test6.java:18)
at test.Test6.main(Test6.java:6)

如以上代码所示,两个异常信息都不会丢。异常丢失见上文

方法重写时的桥接方法

方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
public Number m() {
return 1;
}
}

class B extends A {
@Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}

对于子类,java 编译器会做如下处理:

1
2
3
4
5
6
7
8
9
10
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}

其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以用下面反射代码来验证

1
2
3
for (Method m : B.class.getDeclaredMethods()) {
System.out.println(m);
}

会输出:

1
2
public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()

匿名内部类

源代码:

1
2
3
4
5
6
7
8
9
10
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 额外生成的类
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}

public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();
}
}

匿名内部类会生成一个额外的类

引用局部变量的匿名内部类,源代码:

1
2
3
4
5
6
7
8
9
10
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x; // 多一个属性存储,不用再调外面的局部变量
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
1
2
3
4
5
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}

注意

这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化 -> ==保证 匿名内部类中的属性外部传递的局部变量 必须保证数据一致性==

5. 类加载

类加载阶段

从大方面将可以将类加载分为三个阶段

  1. 加载
  2. 链接
  3. 初始化

加载

  • 将类的字节码载入方法区中,内部采用 C++ 的数据结构 -> instanceKlass 描述 java 类( java 不能直接访问 instanceKlass 需要经过转换的过程 ),它的重要 field 有:
    • _java_mirror 即 java 的类镜像,(C++对象 -类对象与Java对象之间的桥梁)例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
    • _super 即父类
    • _fields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行

注意

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在
  • 可以通过前面介绍的 HSDB 工具查看
image-20230130161924281

链接

验证

验证类是否符合 JVM规范,==安全性检查==

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value
3405691578 in class file cn/itcast/jvm/t5/HelloWorld
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
准备

为 static 变量分配空间设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但**属于引用类型,那么赋值就必须在初始化阶段**完成

==以上信息通过反编译字节码文件查看 助记码 就能很清晰的看到==

所以分配空间与赋值是两个不同的动作

解析

将常量池中的符号引用解析为直接引用

类加载都是懒惰式的,没有用到就不会加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 解析的含义
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException,
IOException {
ClassLoader classloader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
// new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}

通过查看进程 id -> 查看对象,如果不调用 new C(); 只能看到,只加载了 类C(*并没有触发解析*),没有加载 类D

image-20230131112923075

类D 是一个未经解析的类,仅仅是常量池中的一个符号,并没有被解析引用内存中哪个位置

image-20230131113343984

触发解析,类D 被加载的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 解析的含义
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException,
IOException {
// ClassLoader classloader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
// Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}

可以发现类C常量池中的类D,已经被解析到内存中的地址

image-20230131113644275

初始化

<cinit>()V 方法

初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

概括得说,类初始化是【懒惰的

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类静态变量只会触发父类的初始化
  • Class.forName -> 反射触发
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化 -> 类链接准备阶段完成
  • 类对象.class 不会触发初始化 -> 类加载阶段
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时

实验证明

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}

验证(实验时请先全部注释,每次只执行其中一个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Load3 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException, IOException {
// // 1. 静态常量不会触发初始化
// System.out.println(B.b);
// // 2. 类对象.class 不会触发初始化
// System.out.println(B.class);
// // 3. 创建该类的数组不会触发初始化
// System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("cn.itcast.jvm.t3.load.B");
// // 5. 不会初始化类 B,但会加载 B、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
System.in.read();


// // 1. 首次访问这个类的静态变量或静态方法时
// System.out.println(A.a);
// // 2. 子类初始化,如果父类还没初始化,会引发
// System.out.println(B.c);
// // 3. 子类访问父类静态变量,只触发父类初始化
// System.out.println(B.a);
// // 4. 会初始化类 B,并先初始化类 A
// Class.forName("cn.itcast.jvm.t3.load.B");


}
}

class A {
static int a = 0;
static {
System.out.println("a init");
}
}

class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}

练习 - 字节码分析

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Load4 {
public static void main(String[] args) {
System.out.println(E.a); // 不会
System.out.println(E.b); // 不会
System.out.println(E.c); // 会 -> 包装类型(引用类型)
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
static {
System.out.println("init E");
}
}

典型应用 - 完成懒惰初始化单例模式

1
2
3
4
5
6
7
8
9
10
11
public final class Singleton {
private Singleton() { }
// 内部类中保存单例 --同样保证了线程安全
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
public static getInstance() {
return LazyHolder.INSTANCE;
}
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

类加载器

以 JDK 8 为例:

名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

类加载器层级关系,在加载类之前,会==向上级询问是否已经加载过该类==,如果没有,还会依次再向上询问,直到都没有加载,才会去加载这个类

–双亲委派类加载模式

Extension ClassLoadergetParents() 的时候显示 null,因为 Bootstrap ClassLoader 不是由 Java 实现的,而是由 C++ 实现的

启动类加载器

Bootstrap ClassLoader 通常都是加载 JAVA_HOME/jre/lib 目录下的类,但是也可以通过改变 JVM 参数来特殊加载我们自定义的类

用 Bootstrap 类加载器加载类:

1
2
3
4
5
public class F {
static {
System.out.println("bootstrap F init");
}
}

执行

1
2
3
4
5
6
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader()); // AppClassLoader ExtClassLoader
}
}

输出

1
2
3
4
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5_1
# -Xbootclasspath/a:. 追加当前目录启动类加载器加载路径
bootstrap F init
null
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以用这个办法替换核心类
    • java -Xbootclasspath:<new bootclasspath> 新路径
    • java -Xbootclasspath/a:<追加路径> 后追加
    • java -Xbootclasspath/p:<追加路径> 前追加,前追加可以替换调核心类的加载。例如通过前追加替换 String Class 类

扩展类加载器

1
2
3
4
5
public class G {
static {
System.out.println("classpath G init");
}
}

执行

1
2
3
4
5
6
7
8
9
10
11
/**
* 演示 扩展类加载器
* 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
* 里面也有一个 G 的类,观察到底是哪个类被加载了
*/
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}

输出

1
2
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2 # 默认在 classpath 路径下

写一个同名的类

1
2
3
4
5
public class G {
static {
System.out.println("ext G init");
}
}

打个 jar 包

1
2
3
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext classpath中也存放一个 G

重新执行 Load5_2

输出

1
2
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44 # 验证了扩展类加载器,同样验证了 双亲委派

双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

注意

这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派
BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

例如:

1
2
3
4
5
6
7
public class Load5_3 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Load5_3.class.getClassLoader()
.loadClass("cn.itcast.jvm.t3.load.H");
System.out.println(aClass.getClassLoader());
}
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级

    sun.misc.Launcher$ExtClassLoader.loadClass()

  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找

  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在

    JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 处

  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了

线程上下文类加载器

在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写 Class.forName("com.mysql.jdbc.Driver") 也是可以让 com.mysql.jdbc.Driver 正确加载,你知道是怎么做的吗?

追踪一下源码:

1
2
3
4
5
6
7
8
9
public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

先不看别的,看看 DriverManager 的类加载器:

1
System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

jdk在某些情况下需要打破双亲委派机制,通过调用 ClassLoader.getSystemClassLoader() 方法 -> 来调用 应用程序类加载器

继续看 loadInitialDrivers() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>
() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1)使用 ServiceLoader 机制加载驱动,即 SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2)使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,**关联的是应用程序类加载器**,因此可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

image-20230130163448952

这样就可以使用

1
2
3
4
5
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

1
2
3
4
5
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

线程上下文类加载器当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}

通过线程上下文类加载器,同样破坏双亲委派机制

自定义类加载器

什么时候需要自定义类加载器

  1. 想加载非 classpath 随意路径中的类文件 -> 想加载任意路径下的类
  2. 都是通过接口来使用实现,希望解耦时,常用在框架设计
  3. 这些类希望予以隔离(不同版本),不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

实现自定义类加载器步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法
    • 注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法

示例:

准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:

两个示例

image-20230131132216900

自定义类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Load7 {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> c1 = classLoader.loadClass("MapImpl1");
Class<?> c2 = classLoader.loadClass("MapImpl1");
System.out.println(c1 == c2);

MyClassLoader classLoader2 = new MyClassLoader();
Class<?> c3 = classLoader2.loadClass("MapImpl1");
System.out.println(c1 == c3); // 不同类加载器加载的类不同

c1.newInstance();
}
}

class MyClassLoader extends ClassLoader {

@Override // name 就是类名称
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "e:\\myclasspath\\" + name + ".class";

try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);

// 得到字节数组
byte[] bytes = os.toByteArray();

// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);

} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}

运行期优化

即时编译

JIT

分层编译

(TieredCompilation)

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JIT1 {
// -XX:+PrintCompilation -XX:-DoEscapeAnalysis
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
0 96426
1 52907
2 44800
3 119040
4 65280
5 47360
6 45226
7 47786
8 48640
9 60586
10 42667
11 48640
12 70400
13 49920
14 49493
15 45227
16 45653
17 60160
18 58880
19 46080
20 47787
21 49920
22 54187
23 57173
24 50346
25 52906
26 50346
27 47786
28 49920
29 64000
30 49067
31 63574
32 63147
33 56746
34 49494
35 64853
36 107520
37 46933
38 51627
39 45653
40 103680
41 51626
42 60160
43 49067
44 45653
45 49493
46 51626
47 49066
48 47360
49 50774
50 70827
51 64000
52 72107
53 49066
54 46080
55 44800
56 46507
57 73813
58 61013
59 57600
60 83200
61 7024204
62 49493
63 20907
64 20907
65 20053
66 20906
67 20907
68 21333
69 22187
70 20480
71 21760
72 19200
73 15360
74 18347
75 19627
76 17067
77 34134
78 19200
79 18347
80 17493
81 15360
82 18774
83 17067
84 21760
85 23467
86 17920
87 17920
88 18774
89 18773
90 19200
91 20053
92 18347
93 22187
94 17920
95 18774
96 19626
97 33280
98 20480
99 20480
100 18773
101 47786
102 17493
103 22614
104 64427
105 18347
106 19200
107 26027
108 21333
109 20480
110 24747
111 32426
112 21333
113 17920
114 17920
115 19200
116 18346
117 15360
118 24320
119 19200
120 20053
121 17920
122 18773
123 20053
124 18347
125 18347
126 22613
127 18773
128 19627
129 20053
130 20480
131 19627
132 20053
133 15360
134 136533
135 43093
136 853
137 853
138 853
139 853
140 854
141 853
142 853
143 853
144 853
145 853
146 853
147 854
148 853
149 853
150 854
151 853
152 853
153 853
154 1280
155 853
156 853
157 854
158 853
159 853
160 854
161 854
162 853
163 854
164 854
165 854
166 854
167 853
168 853
169 854
170 853
171 853
172 853
173 1280
174 853
175 1280
176 853
177 854
178 854
179 427
180 853
181 854
182 854
183 854
184 853
185 853
186 854
187 853
188 853
189 854
190 1280
191 853
192 853
193 853
194 853
195 854
196 853
197 853
198 853
199 854

探究原因 ->

JVM 将执行状态分成了 5 个层次:

  1. 0 层,解释执行(Interpreter)-> 到达一定阈值执行 编译
  2. 1 层,使用 C1 即时编译器编译执行(不带 profiling)
  3. 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
  4. 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
  5. 4 层,使用 C2 即时编译器编译执

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),将其优化

C1 -> 提升 5倍

C2 -> 提升 10倍

刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果

简述:在C2中进行逃逸分析,发现 new Object(); 并不会被外部引用,判断其可以逃逸,就将 new Object(); 部分代码干脆直接替换掉

参考资料

https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machine-performance-enhancements.html#GUID-D2E3DC58-D18B-4A6C-8167-4A1DFB4888E4

方法内联

(Inlining)

1
2
3
private static int square(final int i) {
return i * i;
}
1
System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置

1
System.out.println(9 * 9);

还能够进行常量折叠(constant folding)的优化

1
System.out.println(81);

实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JIT2 {
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印inlining 信息
// -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
// -XX:+PrintCompilation 打印编译信息
public static void main(String[] args) {
int x = 0;
for (int i = 0; i < 500; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
x = square(9);
}
long end = System.nanoTime();
System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
}
}
private static int square(final int i) {
return i * i;
}
}
字段优化

JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/

创建 maven 工程,添加依赖如下

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>

编写基准测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
@Warmup(iterations = 2, time = 1) // 热身
@Measurement(iterations = 5, time = 1) // 热身几轮
@State(Scope.Benchmark)
public class Benchmark1 {
int[] elements = randomInts(1_000);
private static int[] randomInts(int size) {
Random random = ThreadLocalRandom.current();
int[] values = new int[size];
for (int i = 0; i < size; i++) {
values[i] = random.nextInt();
}
return values;
}
@Benchmark
public void test1() {
for (int i = 0; i < elements.length; i++) {
doSum(elements[i]);
}
}
@Benchmark
public void test2() {
int[] local = this.elements;
for (int i = 0; i < local.length; i++) {
doSum(local[i]);
}
}
@Benchmark
public void test3() {
for (int element : elements) {
doSum(element);
}
}
static int sum = 0;
@CompilerControl(CompilerControl.Mode.INLINE)
static void doSum(int x) {
sum += x;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(Benchmark1.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}

首先启用 doSum 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好):

1
2
3
4
Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 2420286.539 390747.467 ops/s
t.Benchmark1.test2 thrpt 5 2544313.594 91304.136 ops/s
t.Benchmark1.test3 thrpt 5 2469176.697 450570.647 ops/s

接下来禁用 doSum 方法内联

1
2
3
4
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
sum += x;
}

测试结果如下:

1
2
3
4
Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 296141.478 63649.220 ops/s
t.Benchmark1.test2 thrpt 5 371262.351 83890.984 ops/s
t.Benchmark1.test3 thrpt 5 368960.847 60163.391 ops/s

分析:

在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化:

如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码):

1
2
3
4
5
6
7
@Benchmark
public void test1() {
// elements.length 首次读取会缓存起来 -> int[] local
for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
sum += elements[i]; // 1000 次取下标 i 的元素 <- local
}
}

可以节省 1999 次 Field 读取操作

但如果 doSum 方法没有内联,则不会进行上面的优化

练习:在内联情况下将 elements 添加 volatile 修饰符,观察测试结果

反射优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Reflect1 {
public static void foo() {
System.out.println("foo...");
}
public static void main(String[] args) throws Exception {
Method foo = Reflect1.class.getMethod("foo");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t", i);
foo.invoke(null);
}
System.in.read();
}
}

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor(方法访问器)NativeMethodAccessorImpl(本地) 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import sun.reflect.misc.ReflectUtil;
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object target, Object[] args)
throws IllegalArgumentException, InvocationTargetException {
// inflationThreshold 膨胀阈值,默认 15
if (++this.numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass()))
{
// 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
MethodAccessorImpl generatedMethodAccessor =
(MethodAccessorImpl)
(new MethodAccessorGenerator())
.generateMethod(
this.method.getDeclaringClass(),
this.method.getName(),
this.method.getParameterTypes(),
this.method.getReturnType(),
this.method.getExceptionTypes(),
this.method.getModifiers()
);
this.parent.setDelegate(generatedMethodAccessor);
}
// 调用本地实现
return invoke0(this.method, target, args);
}
void setParent(DelegatingMethodAccessorImpl parent) {
this.parent = parent;
}
private static native Object invoke0(Method method, Object target, Object[]
args);
}

当调用到第 16 次(从0开始算,达到阈值)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为 sun.reflect.GeneratedMethodAccessor1 可以使用阿里的 arthas 工具:

1
2
3
4
java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1

选择 1 回车表示分析该进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
[INFO] arthas home: /root/.arthas/lib/3.1.1/arthas
[INFO] Try to attach process 13065
[INFO] Attach process 13065 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version 3.1.1
pid 13065
time 2019-06-10 12:23:54

再输入【jad + 类名】来进行反编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ jad sun.reflect.GeneratedMethodAccessor1
ClassLoader:
+-sun.reflect.DelegatingClassLoader@15db9742
+-sun.misc.Launcher$AppClassLoader@4e0e2f2a
+-sun.misc.Launcher$ExtClassLoader@2fdb006e
Location:
/*
* Decompiled with CFR 0_132.
*
* Could not load the following classes:
* cn.itcast.jvm.t3.reflect.Reflect1
*/
package sun.reflect;
import cn.itcast.jvm.t3.reflect.Reflect1;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;
public class GeneratedMethodAccessor1
extends MethodAccessorImpl {
/*
* Loose catch block
* Enabled aggressive block sorting
* Enabled unnecessary exception pruning
* Enabled aggressive exception aggregation
* Lifted jumps to return sites
*/
public Object invoke(Object object, Object[] arrobject) throws
InvocationTargetException {
// 比较奇葩的做法,如果有参数,那么抛非法参数异常
block4 : {
if (arrobject == null || arrobject.length == 0) break block4;
throw new IllegalArgumentException();
}
try {
// 可以看到,已经是直接调用了,不是反射调用
Reflect1.foo();
// 因为没有返回值
return null;
}
catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}
catch (ClassCastException | NullPointerException runtimeException) {
throw new IllegalArgumentException(Object.super.toString());
}
}
}
Affect(row-cnt:1) cost in 1540 ms.

注意

通过查看 ReflectionFactory 源码可知

  • sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
  • sun.reflect.inflationThreshold 可以修改膨胀阈值(环境变量设置)
image-20230131135234604

6. JMM

Java Memory Model(Java内存模型), 围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。

JMM结构规范

JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

很多人将【java 内存结构】与【java 内存模型】傻傻分不清,【java 内存模型】是 Java Memory Model(JMM)的意思。

关于它的权威解释,请参考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfdspec-oth-JSpec/memory_model-1_0-pfd-spec.pdf?AuthParam=1562811549_4d4994cbd5b59d964cd2907ea22ca08b

简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障

原子性

原子性在学习线程时讲过,下面来个例子简单回顾一下:问题提出,两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

1
2
3
4
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量

而对应 i– 也是类似:

1
2
3
4
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和线程内存中进行数据交换:

image-20230130165217833

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

1
2
3
4
5
6
7
8
9
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
getstatic i // 线程1-获取静态变量i的值 线程内i=1
iconst_1 // 线程1-准备常量1
isub // 线程1-自减 线程内i=0
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=0

但多线程下这 8 行代码可能交错运行(为什么会交错?思考一下):

出现负数的情况:

1
2
3
4
5
6
7
8
9
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

出现正数的情况:

1
2
3
4
5
6
7
8
9
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

解决方案

synchronized (同步关键字)

语法

1
2
3
synchronized( 对象 ) {
要作为原子操作代码
}

synchronized 解决并发问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
synchronized (obj) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}

如何理解:你可以把 obj 想象成一个房间,线程 t1,t2 想象成两个人。

当线程 t1 执行到 synchronized(obj) 时就好比 t1 进入了这个房间,并反手锁住了门,在门内执行 count++ 代码

这时候如果 t2 也运行到了 synchronized(obj) 时,它发现门被锁住了,只能在门外等待

当 t1 执行完 synchronized{} 块内的代码,这时候才会解开门上的锁,从 obj 房间出来。t2 线程这时才可以进入 obj 房间,反锁住门,执行它的 count– 代码

注意:

  • 上例中 t1 和 t2 线程必须用 synchronized 锁住同一个 obj 对象,如果 t1 锁住的是 m1 对象,t2 锁住的是 m2 对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果
  • 针对 i++ 只有四条虚拟机指令加锁,频繁的加锁与解锁造成极大的性能开销,通过思考锁粒度,来优化加锁解锁的结构
1
2
3
4
5
synchronized (obj) {
for (int j = 0; j < 5000; j++) {
i--;
}
}

可见性

退不出循环问题

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

1
2
3
4
5
6
7
8
9
10
11
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}

为什么呢?分析一下:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
image-20230130225619474
  1. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
image-20230130225647369
  1. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
image-20230130225722127

解决方案

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

可见性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况:

上例从字节码理解是这样的:

1
2
3
4
5
6
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i– ,只能保证看到最新值,不能解决指令交错

1
2
3
4
5
6
7
8
9
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

注意

  • synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低

  • 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?println()底层有 synchronized{}关键字锁

有序性

诡异结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

分析

  1. 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  2. 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
  3. 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

但实际上结果还有可能是 0

==这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行num = 2==

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:

借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress

1
2
mvn archetype:generate -DinteractiveMode=false -
DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-testarchetype -DgroupId=org.sample -DartifactId=test -Dversion=1.0

创建 maven 项目,提供如下测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
int num = 0;
boolean ready = false;
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}

必须打包执行

1
2
mvn clean install
java -jar target/jcstress.jar

会输出我们感兴趣的结果,摘录其中一次结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.
2 matching test results.
[OK] test.ConcurrencyTest
(JVM args: [-XX:-TieredCompilation])
Observed state Occurrences Expectation Interpretation
0 1,729 ACCEPTABLE_INTERESTING !!!!
1 42,617,915 ACCEPTABLE ok
4 5,146,627 ACCEPTABLE ok
[OK] test.ConcurrencyTest
(JVM args: [])
Observed state Occurrences Expectation Interpretation
0 1,652 ACCEPTABLE_INTERESTING !!!!
1 46,460,657 ACCEPTABLE ok
4 4,571,072 ACCEPTABLE ok

可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了

解决方案

volatile 修饰的变量,可以禁用指令重排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}

结果为:

1
2
3
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.
0 matching test results.

有序性理解

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

1
2
3
4
5
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时的操作
j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

1
2
i = ...; // 较为耗时的操作
j = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性,例如著名的 double-checked locking 模式实现单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) {
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

但在多线程环境下,上面的代码是有问题的, INSTANCE = new Singleton() 对应的字节码为:

1
2
3
4
5
0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field
INSTANCE:Lcn/itcast/jvm/t4/Singleton;

其中 4 7 两步的顺序不是固定的,也许 jvm 会优化为:先将引用地址赋值给 INSTANCE 变量后,再执行构造方法,如果两个线程 t1,t2 按如下时间序列执行:

1
2
3
4
5
6
时间1 t1 线程执行到 INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0 处)
时间3 t1 线程将引用地址赋值给 INSTANCE,这时 INSTANCE != null(7 处)
时间4 t2 线程进入getInstance() 方法,发现 INSTANCE != null(synchronized块外),直接
返回 INSTANCE
时间5 t1 线程执行Singleton的构造方法(4 处)

这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

happens-before

happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
1
2
3
4
5
6
7
8
9
10
11
12
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
1
2
3
4
5
6
7
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
1
2
3
4
5
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
1
2
3
4
5
6
7
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z

变量都是指成员变量或静态成员变量

参考:上文

CAS 与 原子类

CAS

CAS 即 Compare and Swap ,它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行 +1 操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 需要不断尝试
while(true) {
int 旧值 = 共享变量 ; // 比如拿到了当前值 0
int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
/*
这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
compareAndSwap 返回 false,重新尝试,直到:
compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
*/
if( compareAndSwap ( 旧值, 结果 )) {
// 成功,退出循环
}
}

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,下面是直接使用 Unsafe 对象进行线程安全保护的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestCAS {
public static void main(String[] args) throws InterruptedException {
DataContainer dc = new DataContainer();
int count = 5;
Thread t1 = new Thread(() -> {
for (int i = 0; i < count; i++) {
dc.increase();
}
});
t1.start();
t1.join();
System.out.println(dc.getData());
}
}
class DataContainer {
private volatile int data;
static final Unsafe unsafe;
static final long DATA_OFFSET;
static {
try {
// Unsafe 对象不能直接调用,只能通过反射获得
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
try {
// data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
DATA_OFFSET =
unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public void increase() {
int oldValue;
while(true) {
// 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
oldValue = data;
// cas 尝试修改 data 为 旧值 + 1,如果期间旧值被别的线程改了,返回 false
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue +
1)) {
return;
}
}
}
public void decrease() {
int oldValue;
while(true) {
oldValue = data;
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue -
1)) {
return;
}
}
}
public int getData() {
return data;
}
}

乐观锁与悲观锁

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会

原子操作类

juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的

可以使用 AtomicInteger 改写之前的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建原子整数对象
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndIncrement(); // 获取并且自增 i++
// i.incrementAndGet(); // 自增并且获取 ++i
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndDecrement(); // 获取并且自减 i--
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}

synchronized 优化

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的 哈希码分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位线程锁记录指针重量级锁指针线程ID 等内容

轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

这就好比:

学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没有竞争,继续上他的课。如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程 A 随即升级为重量级锁,进入重量级锁的流程。而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来

假设有两个方法同步块,利用同一个对象加锁

1
2
3
4
5
6
7
8
9
10
11
12
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}

每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

线程 1 对象 Mark Word 线程 2
访问同步块 A,把 Mark 复制到线程 1 的锁记录 01(无锁) -
CAS 修改 Mark 为线程 1 锁记录地址 01(无锁) -
成功(加锁) 00(轻量锁)线程 1 锁记录地址 -
执行同步块 A 00(轻量锁)线程 1 锁记录地址 -
访问同步块 B,把 Mark 复制到线程 1 的锁记录 00(轻量锁)线程 1 锁记录地址 -
CAS 修改 Mark 为线程 1 锁记录地址 00(轻量锁)线程 1 锁记录地址 -
失败(发现是自己的锁) 00(轻量锁)线程 1 锁记录地址 -
锁重入 00(轻量锁)线程 1 锁记录地址 -
执行同步块 B 00(轻量锁)线程 1 锁记录地址 -
同步块 B 执行完毕 00(轻量锁)线程 1 锁记录地址 -
同步块 A 执行完毕 00(轻量锁)线程 1 锁记录地址 -
成功(解锁) 01(无锁) -
- 01(无锁) 访问同步块 A,把 Mark 复制到线程 2 的锁记录
- 01(无锁) CAS 修改 Mark 为线程 2 锁记录地址
- 00(轻量锁)线程 2 锁记录地址 成功(加锁)
-

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

1
2
3
4
5
6
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
线程 1 对象 Mark 线程 2
访问同步块,把 Mark 复制到线程 1 的锁记录 01(无锁) -
CAS 修改 Mark 为线程 1 锁记录地址 01(无锁) -
成功(加锁) 00(轻量锁)线程 1 锁记录地址 -
执行同步块 00(轻量锁)线程 1 锁记录地址 -
执行同步块 00(轻量锁)线程 1 锁记录地址 访问同步块,把 Mark 复制到线程 2
执行同步块 00(轻量锁)线程 1 锁记录地址 CAS 修改 Mark 为线程 2 锁记录地址
执行同步块 00(轻量锁)线程 1 锁记录地址 失败(发现别人已经占了锁)
执行同步块 00(轻量锁)线程 1 锁记录地址 CAS 修改 Mark 为重量锁
执行同步块 10(重量锁)重量锁指针 阻塞中
执行完毕 10(重量锁)重量锁指针 阻塞中
失败(解锁) 10(重量锁)重量锁指针 阻塞中
释放重量锁,唤起阻塞线程竞争 01(无锁) 阻塞中
- 10(重量锁) 竞争重量锁
- 10(重量锁) 成功(加锁)
-

重量锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次反之,就少自旋甚至不自旋,总之,比较智能

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)
  • Java 7 之后不能控制是否开启自旋功能,jvm内部控制自旋

自旋重试成功的情况

线程 1 (cpu 1 上) 对象 Mark 线程 2 (cpu 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
- 10(重量锁)重量锁指针 成功(加锁)
- 10(重量锁)重量锁指针 执行同步块
-

自旋重试失败的情况

线程 1(cpu 1 上) 对象 Mark 线程 2(cpu 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞
-

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS

  • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
  • 访问对象的 hashCode 也会撤销偏向锁
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
  • 撤销偏向和重偏向都是批量进行的,以类为单位
  • 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
  • 可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁

参考文章

https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf

假设有两个方法同步块,利用同一个对象加锁

1
2
3
4
5
6
7
8
9
10
11
12
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
线程 1 对象 Mark
访问同步块 A,检查 Mark 中是否有线程 ID 101(无锁可偏向)
尝试加偏向锁 101(无锁可偏向)对象 hashCode
成功 101(无锁可偏向)线程ID
执行同步块 A 101(无锁可偏向)线程ID
访问同步块 B,检查 Mark 中是否有线程 ID 101(无锁可偏向)线程ID
是自己的线程 ID,锁是自己的,无需做更多操作 101(无锁可偏向)线程ID
执行同步块 B 101(无锁可偏向)线程ID
执行完毕 101(无锁可偏向)对象 hashCode

其它优化

减少上锁时间

同步代码块中尽量短

减少锁的粒度

一个锁拆分为多个锁提高并发度,例如:

  • ConcurrentHashMap
  • LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值
  • LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
锁粗化

多次循环进入同步块不如同步块内多次循环

另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)

1
new StringBuffer().append("a").append("b").append("c");
锁消除

JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作

读写分离

CopyOnWriteArrayList

ConyOnWriteSet

参考:

HotSpot-Synchronization

锁优化

javase-synchronized

锁优化-JVM锁降级

JVM多线程

java-lock

项目部署文档

原生方式

web 服务器:nginx 、apache、tomcat

https://zhuanlan.zhihu.com/p/425790769

前端

Nginx

安装 nginx 服务器:

  1. 用系统自带的软件包管理器快速安装,比如 centos 的 yum
  2. 官网安装(参考文章)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cd 
mkdir services
cd services

curl -o nginx-1.21.6.tar.gz http://nginx.org/download/nginx-1.21.6.tar.gz

tar -zxvf nginx-1.21.6.tar.gz

./configure

yum install pcre pcre-devel -y
yum install openssl openssl-devel -y

./configure

./configure --with-http_ssl_module --with-http_v2_module --with-stream

make

make install

ls /usr/local/nginx/sbin/nginx

配置 Nginx 环境变量

1
2
3
4
5
6
7
8
9
vim /etc/profile
在最后一行添加:
export PATH=$PATH:/usr/local/nginx/sbin

source /etc/profile

nginx

netstat -ntlp 查看启动情况

配置 nginx config 配置文件

1
2
3
4
cd nginx-1.21.6
ls
cp nginx.conf nginx.default.conf
cat nginx.conf
1
2
3
4
5
unzip dist.zip -d user-center-front
cd user-center-front/dist
ls
mv * ../
rm -rf dist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim /usr/local/nginx/nginx.conf
cp nginx.conf nginx.default.conf

###
user root;
....
server {
listen 80;
server_name localhost;
location / {
root /root/services/user-center-front
index index.html index.htm;
}
...
}
...
###

nginx -s reload

后端

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yum install -y java-1.8.0-openjdk*

curl -o apache-maven-3.8.5-bin.tar.gz https://dlcdn.apache.org/maven/maven-3/3.8.5/binaries/apache-maven-3.8.5-bin.tar.gz
# 或者自己上传

tar -zxvf apache-maven-3.8.5-bin.tar.gz
cd apache-maven-3.8.5

vim /etc/profile
在最后一行添加:
export PATH=$PATH:/usr/local/nginx/sbin:/root/services/apache-maven-3.8.5/bin
source /etc/profile

mvn -v
mvn --help
java -version

maven

1
2
git clone xxx 下载代码
mvn package -DskipTests

java

1
2
3
4
5
6
7
8
9
java -jar ./user-center-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

chmod a+x user-center-backend-0.0.1-SNAPSHOT.jar
ls
nohup java -jar ./user-center-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod &

jobs
netstat -ntlp 查看启动情况
jps

宝塔Linux

Linux运维面板 –可视化的操作linux

云服务器开放端口

Centos安装脚本

1
yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh ed8484bec

腾讯云面板安装

1
sudo /etc/init.d/bt default

安装 Nginx

image-20230127102142175

安装java

image-20230127102247741

网站布置

image-20230127102805807 image-20230127102747455 image-20230127102837965 image-20230127102917343

直接访问,BT会帮助修改配置,将IP映射到目录下

配置文件

image-20230127103125822

后端部署

上传文件

image-20230127103315235 image-20230127103446537
1
ps -ef|grep '8080'
image-20230127103854809

腾讯云开放 8080 项目端口

image-20230127104056349

BT白名单放行 8080 项目端口

Docker容器

docker 是容器,可以将项目的环境(比如 java、nginx)和项目的代码一起打包成镜像,所有同学都能下载镜像,更容易分发和移植 –下载镜像 制作镜像 封装镜像

使得启动项目时,不需要敲一大堆命令,而是直接下载镜像、启动镜像就可以了

可以理解为软件安装包

安装 Docker

Docker 安装:https://www.docker.com/get-started/ 或者宝塔安装

1
java -version

BT安装 docker

image-20230127104710471 image-20230127104802350

查看 docker 安装成功

1
docker -v

制作 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM maven:3.5-jdk-8-alpine as builder

# Copy local code to the container image.
WORKDIR /app
COPY pom.xml .
COPY src ./src

# Build a release artifact.
RUN mvn package -DskipTests

# Run the web service on container startup.
CMD ["java","-jar","/app/target/user-center-backend-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"]
1
2
3
4
5
6
7
8
9
10
11
12
FROM nginx

WORKDIR /usr/share/nginx/html/
USER root

COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf

COPY ./dist /usr/share/nginx/html/

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;

# gzip config
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";

root /usr/share/nginx/html;
include /etc/nginx/mime.types;

location / {
try_files $uri /index.html;
}
}

CMD / ENTRYPOINT(附加额外参数)指定运行容器时默认执行的命令

1
sudo git clone xxxx

最好直接上传已经构建好的 dist 目录

根据 Dockerfile 构建对象

1
2
3
4
5
6
7
8
9
# 在有 dockerfile 的目录下

# -t 打标签

# 后端
docker build -t user-center-backend:v0.0.1 .

# 前端
docker build -t user-center-front:v0.0.1 .

Docker 构建优化:减少尺寸、减少构建时间(比如多阶段构建,可以丢弃之前阶段不需要的内容)

1
2
# 查看已拉取所有镜像
docker images

docker run 启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 前端
docker run -p 80:80 -d user-center-frontend:v0.0.1

# 后端
docker run -p 8080:8080 user-center-backend:v0.0.1


# 测试 docker
sudo docker run hello-world
# 会先启动本地镜像,如果本地镜像没有,docker会从远程仓库拉取镜像
# Unable to find image 'hello-world:latest' locally
# latest: Pulling from library/hello-world
# 2db29710123e: Pull complete
# ......

虚拟化

  1. 端口映射:把本机的资源(实际访问地址)和容器内部的资源(应用启动端口)进行关联
  2. 目录映射:把本机的端口和容器应用的端口进行关联
1
2
3
4
5
6
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

docker run -p 80:80 -v /data:/data -d nginx:latest
docker run -p 主机(宿机)端口:容器端口 -v 主机目录:容器目录

docker run -p 127.0.0.1:80:8080/tcp ubuntu bash

启动 docker

1
sudo docker run -p 80:80 -v /data/app:/usr/share/nginx/html -d user-center-frontend:v0.0.1

进入容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 查看已启动的 docker 容器
sudo docker ps
# 查看 docker 容器 id

# 进入容器
docker exec -i -t fee2bbb7c9ee /bin/bash
docker exec -i -t <dockerID> /bin/bash
# -i 交互式 -t 分配终端 (启动哪种命令的终端)

# 退出容器
exit

# 杀死已启动容器
docker kill fee2bbb7c9ee
docker kill <dockerID>

# 重新启动
docker run -p 80:80 -d user-center-frontend:v0.0.1
# -d 以后台的方式启动docker容器
docker run -p 80:80 user-center-frontend:v0.0.1
# 不使用 -d 启动 docker 会使服务器终端直接卡住 类似 jar 启动

# 查看 docker 日志
docker ps
sudo docker logs <dockerID>
sudo docker logs fee2bbb7c9ee
sudo docker logs fee2bbb7c9ee -f
# -f 跟踪实时输出日志

# 删除镜像】
sudo docker rmi -f hello-world
# rmi --remove image

# 后端 docker 启动
docker run -p 8080:8080 user-center-backend:v0.0.1

https://www.runoob.com/docker/docker-tutorial.html

容器平台部署

容器平台的优势

  1. 不用输命令来操作,更方便省事
  2. 不用在控制台操作,更傻瓜式、更简单
  3. 大厂运维,比自己运维更省心
  4. 额外的能力,比如监控、告警、其他(存储、负载均衡、自动扩缩容、流水线)

腾讯云

阿里云

前端webify

webify –web化

web应用托管

比容器化更傻瓜式

  1. 从代码托管平台拉取Code
image-20230128022747566
  1. 自动识别并构建
image-20230128022848541
  1. 每次更新代码,提交代码后,都会触发一次新的部署(避免了每次都要重新构建发布)
image-20230128023013650

后端微信云托管

前端部署

  1. 使用 微信云托管平台
image-20230128023233763
  1. 选择一个环境新建服务
image-20230128023317022
  1. 新建服务
image-20230128023344187
  1. 选择代码 -> 平台自动构建
image-20230128023459054
  1. 手动上传代码包 注意不上传 依赖文件夹 -> 如果项目文件太大,可以只上传 dist docker Dockerfile
image-20230128023711481
  1. 平台自动构建
image-20230128023754360
  1. 平台增值功能 云端调试 回退版本 日志监控 服务配置(LVS) 等
image-20230128024227322 image-20230128024249532 image-20230128024316010
  1. 微信云托管 提供临时 域名 可以直接访问测试
image-20230128024632845

后端部署

  1. 创建新服务
image-20230128023855595
  1. 选择手动上传文件夹(根据 Dockerfile 的命令选择上传 jar 包 or 上传整个项目目录)-> 选择默认配置
image-20230128024109898
  1. 同样会有流水线和代码更新自动构建

  2. 服务告警服务

image-20230128024825050

Vercel

相当于一个云托管服务,可以将项目直接发布在这上面

NEXT.JS conf –NEXT的作者写的

Vercel前端部署

1
2
3
4
5
project-frontend> vercel --prod
y

# 如果没有 Vercel 账号需要注册

自动部署发布

image-20230128100138799

Vercel可以(免备案!!!)

且 Vercel 免费

后端微信云托管

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM maven:3.5-jdk-8-alpine as builder

# Copy local code to the container image.
WORKDIR /app
COPY pom.xml .
COPY src ./src

# Build a release artifact.
RUN mvn package -DskipTests

# Run the web service on container startup.
CMD ["java","-jar","/app/target/yupao-backend-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"]

买个域名就可以直接用,直接使用别人的容器

绑定域名

前端项目访问:用户输入网址 => 域名解析服务器(把网址解析为 ip 地址 / 交给其他的域名解析服务) => 服务器 =>(防火墙)=> nginx 接收请求,找到对应的文件,返回文件给前端 => 前端加载文件到浏览器中(js、css) => 渲染页面

BT面板有防火墙 -> 需要 nginx 或 BT面板开启端口(直接在 nginx 中开启 接受域名的请求即可,详细见下文)

后端项目访问:用户输入网址 => 域名解析服务器 => 服务器 => nginx 接收请求 => 后端项目(比如 8080端口)

nginx 反向代理:替服务器接收请求,转发请求

BT绑定域名

配置前端域名

  1. 域名注册商 例如 腾讯云使用 DNSPOD 解析 -> 添加记录
image-20230128025223232
  1. BT面板给 nginx 添加域名站点
image-20230128025541364

注意 直接访问 服务器 IP 需要能找到相对应的 index.html 文件

如果在访问域名与IP url 路径时,如果匹配不了相对应的路由路径,就会直接去匹配默认的 index.html

配置后端域名

  1. DNSPOD配置
image-20230128030233837
  1. 处理端口号问题
image-20230128030310670
  1. 访问域名 -> 直接访问 80 端口 -> 访问 nginx -> nginx 接受请求后 转发请求到指定后端服务端口
  2. 重新新建一个服务 -> 配置 nginx config (nginx 不仅能做转发请求还能 -> 改写请求 -> 拦截请求 -> …)
image-20230128030510527
  1. 配置反向代理(替服务器接收请求,转发请求
image-20230128030745898
  1. nginx 统一接受请求,转发请求

Vercel 绑定域名

跨域问题解决

跨域问题产生原因

浏览器为了用户的安全,仅允许向 同域名、同端口 的服务器发送请求。

==为了检测跨域 -> 浏览器会在发送正式请求之前 -> 发送预检请求 [OPTIONS] -> 预检请求的请求类型是 [OPTIONS] -> 预检请求的作用是提前探路==

什么情况下会发送预检请求?

  1. 检查是否跨域
  2. 请求域名与当前网页域名、端口冲突的时候 -> 发送预检请求

TODO什么情况下会发送预检请求

解决跨域 -> 让服务器告诉浏览器:允许跨域(返回 cross-origin-allow 响应头)

  1. 把域名、端口改成相同的

让服务器告诉浏览器:允许跨域(返回 cross-origin-allow 响应头)

  1. 网关支持(Nginx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
location ^~ /api/ {
proxy_pass http://127.0.0.1:8080/api/;
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers '*';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}

注意:尽量不要使用

1
add_header Access-Control-Allow-Origin *;

* 虽然能允许所有域名跨域,但是不能与 withCredentials 自动带 cookie 的请求头 同时使用

如果要允许所有请求跨域 -> 可以使用 $http_origin -> 来代替 * 允许全部的选项

注意 nginx 配置中 预检请求也要加上 允许 $http_origin

==如果要在网关进行请求过滤 -> 可以在 nginx 中加上 if -> 来判断是否需要拦截该请求==

  1. 修改后端服务
    1. 配置 @CrossOrigin 注解 @CrossOrigin(origins = { "http://xxx.xxx.cn" }, methods = { RequestMethod.DELETE, RequestMethod.POST })
    2. 添加 web 全局请求拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class WebMvcConfg implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
//当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
.allowedOrigins("http://localhost:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082", "http://127.0.0.1:8083")
//是否允许证书 不再默认开启
.allowCredentials(true)
//设置允许的方法
.allowedMethods("*")
//跨域允许时间
.maxAge(3600);
}
}

较推荐

  1. 定义新的 corsFilter Bean,参考:https://www.jianshu.com/p/b02099a435bd

把域名、端口改成相同的

让服务器告诉浏览器:允许跨域(返回 cross-origin-allow 响应头)

网关支持(Nginx)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
location ^~ /api/ {
proxy_pass http://127.0.0.1:8080/api/;
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers '*';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}

修改后端服务

后端提供跨域支持

@CrossOrigin

@CrossOrigin(origins = { "http://xxx.xxx.cn" }, methods = { RequestMethod.DELETE, RequestMethod.POST })

web 全局请求拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class WebMvcConfg implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
//当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
.allowedOrigins("http://localhost:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082", "http://127.0.0.1:8083")
//是否允许证书 不再默认开启
.allowCredentials(true)
//设置允许的方法
.allowedMethods("*")
//跨域允许时间
.maxAge(3600);
}
}

定义新的 corsFilter Bean

参考:https://www.jianshu.com/p/b02099a435bd

JUC

java.util.concurrent

主要内容

  1. 进程
  2. 线程
  3. 并发
  4. 并行

基础业务不常用,但是针对性能,tomcat服务器、RPC框架、Dubbo、消息中间件RocketMQ、RabbitMQ….这些框架底层多使用并发编程

学习内容 –大纲

  1. 进程
  2. 线程
  3. 并发
    1. 并发之共享模型
      • 管程 –悲观锁(阻塞)
      • JVM
        • 原子性
        • 可见性
        • 有序性
      • 无锁 –乐观锁(非阻塞)
      • 不可变
      • 并发工具
        • 线程池
        • JUC
          • Lock
          • Semaphore
          • CountdownLatch
          • CyclicBarrier
          • ConcurrentHashMap
          • ConcurrentLinkedQueue
          • BlockingQueue
          • CopyOnWriteArrayList
        • disruptor
        • guava Guava 是一套来自Google的核心Java库
      • 异步编程
        • CompletableFuture
        • 反应式
          • project-reactor
          • spring webflux
    2. 并发之非共享模型
      • 私有
      • Actor –弱一致性
        • akka
    3. 并行
      • 函数式编程
      • 并行式编程
        • 映射
        • 归约
    4. 应用
      • 效率
      • 限流
      • 同步
      • 异步
      • 缓存
      • 队列
      • 分治
      • 统筹
    5. 原理
    6. 涉及设计模式
      • Balking
      • Guarded Suspension
      • 控制顺序
      • 两阶段退出
      • WorkThread
      • Thread per Message
      • 生产者/消费者
    7. 其他领域

预备知识

  • 线程安全问题,需要你接触过Java Web开发、Jdbc开发、Web服务器、分布式框架时才会遇到
  • 基于JDK8,最好对函数式编程、lambda有一定了解
  • 采用了slf4打印日志,这是好的实践
  • 采用了lombok简化java bean编写
  • 给每个线程好名字,这也是一项好的实践

概念

chapter-1 进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的
  • 当一个程序(window下就是一个.exe文件由指令与数据组成)被运行,需要将指令交给CPU,从磁盘加载这个程序的代码至内存,供CPU读取,这时就开启了一个进程。我们常说的程序是指磁盘上静态存储的程序,而进程是已经被加载到CPU活动的程序
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
  • Java中,线程作为最小调度单位,进程作为资源分配的最小单位(管理内存,管理IO)。在windows中进程是不活动的,只是作为线程的容器

进程与线程对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为PC(Inter–process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

chapter-2 并行与并发

并发

单核cpu下,线程实际还是串行执行的。操作系统中有一个组件叫做操作系统的任务调度器,将cpu的时间片
(windows下时间片最小约为15毫秒)分给不同的线程使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。总结为一句话就是:微观串行,宏观并行
一般会将这种线程轮流使用CPU的做法称为并发 –concurrent

image-20221216072549740 image-20221216072334132

并行

通常是既有并发又有并行 –因为CPU核数不会比线程数多

多核CPU下,每个核(core)都可以进行调度运行线程,这时候线程是可以并行的

image-20221216072712620 image-20221216072635208

引用Rob Pike的一段描述:

  • 并发(concurrent)是同一时间应对(dealing with) 多件事情的能力
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

并发与并行的举例描述:

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发

  • 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一 个人用锅时,另一个人就得等待)

  • 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行

Rob Pike资料

  • golang语言的创造者
  • Rob Pike-百度百科

应用

多线程应用概述

以调用方角度来讲

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

应用设计案例

多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如 果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…

  • 在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
  • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
  • ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
实际案例

同步调用

1
2
3
4
public static void main(String[] args) {
FileReader.read(Constants.MP4_FULL_PATH);
log.debug("do other things ...");
}

异步调用

1
2
3
4
public static void main(String[] args) {
new Thread(() -> FileReader.read(Constants.MP4_FULL_PATH)).start();
log.debug("do other things ...");
}

多线程提高效率

详细见应用

结论

多线程在不同操作系统上的优势结论 –Linux单核

  1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
  2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
    • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分(参考后文的【阿姆达尔定律】)
    • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
  3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

chapter-3 Java操作多线程

内容概述

  • 创建和运行线程
  • 查看线程
  • 线程 API
  • 线程状态

创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Thread 创建多线程
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
log.debug("running");
}
};
t1.setName("t1");
t1.start();

log.debug("running");
}


// 使用 runnable 创建多线程
// Thread 代表线程
// Runnable 可运行的任务(线程要执行的代码)
public static void main(String[] args) {
// 任务对象
Runnable r = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};

// 线程对象
Thread t2 = new Thread(r, "t2");
t2.start();
}


// 使用 lambda 表达式创建多线程
public static void main(String[] args) {
Runnable r = () -> {
log.debug("running");
};

Thread t1 = new Thread(r, "t1");
t1.start();

Thread t2 = new Thread(() -> log.debug("running"), "t2");
}


// 使用 FutureTask 创建多线程 --线程间通信
public static void main(String[] args) throws ExecutionException, InterruptedException {
// FutureTask 来接收线程运行结果
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running");
Thread.sleep(1000);
return -1;
}
});

Thread t2 = new Thread(task, "t2");
t2.start();

log.debug("{}", task.get());
}

Thread 对比 Runnable

Thread 与 Runnable 的原理和关系

  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

多线程运行

  • 多线程交替运行
  • 多线程运行顺序先后不由我们控制,而是由操作系统的任务调度器控制

杀死线程

杀死线程

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程 tasklist | findstr java
  • taskkill 杀死进程 taskkill /F /PID 17777 /F 强制杀死

Linux

  • ps -fe 查看所有进程 ps -fe | grep java
  • ps -fT -p <PID> 查看某个进程(PID)的所有线程
  • kill 杀死进程 kill 4202
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID> 查看某个进程(PID)的所有线程 top -H -p 4262

Java

  • jps 命令查看所有 Java 进程
  • jstack <PID> 查看某个 Java 进程(PID)的所有线程状态 -只查询某一刻时的运行情况(类似于快照)
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置 –见资料

win + r jconsole

1
2
3
4
5
6
7
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证 java类

java -Djava.rmi.server.hostname=192.168.187.130 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=12345 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false Test2

# 关闭防火墙
service iptables status
service iptables stop
image-20230108181502682

多线程栈帧原理

多线程栈帧原理

Java Virtual Machine Stacks (Java 虚拟机栈)

JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

栈内存在方法执行完后直接释放,堆内存需要垃圾回收机制来回收内存

image-20230108182522250

Debug –Java中线程处于就绪与运行状态 都显示RUNNING

image-20230108182600373

main主线程与t1线程都有自己的栈帧内存,是互不相干的 –栈帧是以线程为单位,相互独立的

线程上下文切换

线程上下文切换(Thread Context Switch)

被动/主动 上下文切换

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

Thread Context Switch 图解流程

image-20230108183132274

线程方法

方法名 static 功能说明 功能说明
start() 启动一个新线程,在新的线程运行 run 方法中的代码 start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException
run() 新线程启动后会调用的方法 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结束,最多等待 n 毫秒
getId() 获取线程长整型的 id id 唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted() 判断是否被打断 不会清除 打断标记
isAlive() 线程是否存活(还没有运行完 毕)
interrupt() 打断线程 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记;如果打断的正在运行的线程,则会设置 打断标记;park 的线程被打断,也会设置 打断标记
interrupted() static 判断当前线程是否被打断 会清除 打断标记
currentThread() static 获取当前正在执行的线程
sleep(long n) static 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
yield() static 提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试

start 对比 run

start 与 run 对比

run

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things ...");
}

输出

1
2
3
4
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j(topic = "c.Test9")
public class Test9 {
// 多线程中 run 与 start API
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running");
}
};
// 主线程调用 run 方法 --本质是主线程调用匿名内部类的方法
// (当前类可以调用匿名内部类的所有方法,匿名内部类继承了Thread类,并重写了 run 方法)
// t1.run();
// t1 线程调用 run 方法
t1.start();

}
}

输出

1
2
3
4
19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms

程序在 t1 线程运行, FileReader.read() 方法调用是异步的

小结

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

Java线程状态(初窥)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j(topic = "c.Test10")
public class Test10 {
// 线程状态详解
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running....");
}
};
System.out.println(t1.getState());
// NEW
t1.start();
System.out.println(t1.getState());
// RUNNABLE --可被 CPU 调用执行
t1.start();
// 线程状态变成 RUNNABLE 就不能再被 start 否则抛出异常 --线程多次调用问题
// Exception in thread "main" java.lang.IllegalThreadStateException
// at java.lang.Thread.start(Thread.java:708)
// at test.Test10.main(Test11.java:18)

}
}

sleep 对比 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting –阻塞状态不会分配到CPU时间片 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行 –取决于操作系统的任务调度器
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 TimeUnit.SECONDS.sleep(1);

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable –就绪状态还是有机会分配到CPU时间片 就绪状态,然后调度执行其它线程
  2. 但是让出了CPU使用权,并不一定会使CPU去运行其他线程,具体的实现依赖于操作系统的任务调度器

线程优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器甚至可以忽略
  • 如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Slf4j(topic = "c.Test14")
public class Test14 {
// yield API 学习
// yield 让出 --yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后CPU调度执行其它线程
// 但具体的实现依赖于操作系统的任务调度器,yield 只是在此刻,将时间片让出,后续如何分配时间片还是任务调度器控制
// 任务调度器在控制时间片时,会考虑 Runnable 状态的线程,但不会考虑 sleep 中 TIMED_WAITING 的线程
// yield 与 线程优先级学习
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
// Thread.yield();
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}

Join使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j(topic = "c.Test16")
public class Test16 {

// JOIN API 的使用
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
},"t1");
t1.start();
// 主线程中调用 t1.join(); 主线程 等待 t1线程执行结束后,在进行后续线程执行
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
}

分析:

  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

解决:

  • sleep
  • join,加在 t1.start() 之后即可

限时等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j(topic = "c.Test18")
public class Test18 {
// 限时同步
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});

long start = System.currentTimeMillis();
t1.start();

// 线程执行结束会导致 join 结束
log.debug("join begin");
t1.join(3000);
// t1.join(1000);
// 限时等待,当需要等待的线程执行结束,当前线程也会提前结束等待
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}

简单应用

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步
image-20230113223121222

==等待多个结果==

问,下面代码 cost 大约多少秒?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

分析如下

  • 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
  • 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s

如果颠倒两个 join 呢?

最终都是输出

1
20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005
image-20230113223334077

Interrupt使用

Interrupt打断三种状态的线程

  • sleep
  • wait
  • join

这几个方法都会让线程进入阻塞状态

打断 sleep 的线程, 会清空打断状态,以 sleep 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Slf4j(topic = "c.Test19")
public class Test19 {
// interrupt API 使用 --打断阻塞中的线程 与 正在运行中的线程
// join 的底层原理就是 wait
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000);
// slepp, wait, join 会在打断后 清空 isInterrupted 打断标记 --重置为 false
// 而打断正常运行的线程,isInterrupted 标记就是 true
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");

t1.start();
log.debug("打断标记1:{}", t1.isInterrupted());
// 主线程等 t1 进入睡眠后 打断 t1 线程
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
// 被打断过就是 true 否则 就是 false
Thread.sleep(1000);
log.debug("打断标记2:{}", t1.isInterrupted());
/*
20:52:47.795 c.Test19 [t1] - sleep...
20:52:47.795 c.Test19 [main] - 打断标记1:false
20:52:48.806 c.Test19 [main] - interrupt
20:52:48.806 c.Test19 [main] - 打断标记2:true
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at test.Test19.lambda$main$0(Test19.java:13)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0

slepp, wait, join 会在打断后 清空 isInterrupted 打断标记 --重置为 false
打断一个sleep线程后调用isInterrupted查看打断标记
有时为false有时为true,而且为true的情况反而更多,false需要多次运行才会看到

结论,研究一下发现,调用t1.interrupt()后立即调用t1.isInterrupted()
有可能在 t1 还未完成清除打断标记并抛异常的时候就查看打断标记,此时仍然为true,

如果要得到稳定的false,即重置打断标记后的结果,应该在t1.interrupt()后
sleep一会,给点时间,有点像jvm启动后偏向锁延迟设置的意思

20:54:18.456 c.Test19 [t1] - sleep...
20:54:18.456 c.Test19 [main] - 打断标记1:false
20:54:19.459 c.Test19 [main] - interrupt
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at test.Test19.lambda$main$0(Test19.java:13)
at java.lang.Thread.run(Thread.java:748)
20:54:20.460 c.Test19 [main] - 打断标记2:false
Process finished with exit code 0

*/
}
}

Interrupt打断正常运行线程

打断正常运行的线程, 不会清空打断状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j(topic = "c.Test20")
public class Test20 {
// Interrupt 打断正在运行的线程
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();

Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();

/*
20:56:20.428 c.Test20 [main] - interrupt
20:56:20.430 c.Test20 [t1] - 被打断了, 退出循环
*/
}
}

Interrupt打断park线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Slf4j(topic = "c.Test22")
public class Test22 {
// LockSupport.park() 打断park机制
public static void main(String[] args) throws InterruptedException {
testLockSupportPark();
}

private static void testLockSupportPark() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
// 当前线程执行 park 后停住。不执行后续操作
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
// 打断 park 状态后,打断状态改为 true(与正常运行打断相似)
// 打断 park 状态后,后续就不能再 park,park就会失效
// LockSupport.park();
// 但是可以使用 interrupted 获取打断状态,同时将打断状态重置为 false
// Thread.interrupted(); // 将打断标记设为 false
}, "t1");
t1.start();

TimeUnit.SECONDS.sleep(1);
// interrupt 打断当前 park 状态,使线程继续往后执行 同时将打断状态 改为 true
t1.interrupt();

}
// 20:22:41.774 c.Test22 [t1] - park...
// 20:22:42.783 c.Test22 [t1] - unpark...
// 20:22:42.783 c.Test22 [t1] - 打断状态:true
//
// Process finished with exit code 0
}

过时方法

些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,导致锁不能释放,造成线程死锁

方法名 static 功能说明
stop() 停止线程运行 –强制停止
suspend() 挂起(暂停)线程运行 –现在使用 wait notify
resume() 恢复线程运行 –suspend后resume唤醒

守护线程

默认情况下,Java进程需要等所有线程都运行结束后,才会结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j(topic = "c.Test23")
public class Test23 {
// 默认情况下,Java进程需要等所有线程都运行结束后,才会结束。
// 有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.debug("结束程序");
}, "t1");
// 默认设置线程为非守护线程
t1.setDaemon(true); // 设置 t1 为守护线程
// 守护线程的应用 --垃圾回收器线程就是一种守护线程
// Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
t1.start();
TimeUnit.SECONDS.sleep(1);
log.debug("主线程结束");
}
}

线程状态(详解)

操作系统层面的五种状态

image-20230108203745345
  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联 –例如Java中new一个Thread对象,但是没有 start
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态
    • 当线程的 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

Java API层面的六种状态

根据 Thread.State 枚举,分为六种状态

image-20230108204305470
  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞 --操作系统层面的阻塞状态,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
    • BLOCKED –synchronized
    • WAITING –join
    • TIMED_WAITING –sleep
  • TERMINATED 当线程代码运行结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@Slf4j(topic = "c.Test24")
public class Test24 {
// Java API 层面 对应线程的六种状态
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};

Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable

}
}
};
t2.start();

Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();

Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (Test24.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();

Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();

Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (Test24.class) { // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();

try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
// System.in.read();
}

// 20:51:03.214 c.Test24 [t3] - running...
// 20:51:03.724 c.Test24 [main] - t1 state NEW
// 20:51:03.726 c.Test24 [main] - t2 state RUNNABLE
// 20:51:03.726 c.Test24 [main] - t3 state TERMINATED
// 20:51:03.726 c.Test24 [main] - t4 state TIMED_WAITING
// 20:51:03.726 c.Test24 [main] - t5 state WAITING
// 20:51:03.726 c.Test24 [main] - t6 state BLOCKED
}

chapter-4 Monitor管程模型详解

内容概述

  • 共享问题
  • synchronized
  • 线程安全分析
  • Monitor
  • wait/notify
  • 线程状态转换
  • 活跃性
  • Lock

ContextSwitch问题分析

ContextSwitch –> 线程安全问题

问题现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}

分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

1
2
3
4
getstatic	i   // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i– 也是类似:

1
2
3
4
getstatic   i   // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

image-20230108213159158

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

image-20230108213247722

但多线程下这 8 行代码可能交错运行:

出现负数的情况:

image-20230108213342364

出现正数的情况:

image-20230108213946025

临界区 Critical Section 问题

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区

1
2
3
4
5
6
7
8
9
10
11
12
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}

竞态条件 Race Condition 问题

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

synchronized解决方案

互斥应用实现

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronizedLock
  • 非阻塞式的解决方案:原子变量

本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Slf4j(topic = "c.Test1")
public class Test1 {
// 线程不安全演示 --使用 synchronized 对象锁 解决线程不安全问题
static int counter = 0;

static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter++;
}
// log.debug("{}", counter);
}
}, "t1");

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
counter--;
}
// log.debug("{}", counter);
}
}, "t2");

t1.start();
t2.start();
// 主线程等待 t1 t2 线程执行完,在 log 出结果 --可以改成 sleep 使其等待
t1.join();
t2.join();
// TimeUnit.SECONDS.sleep(1);
log.debug("{}", counter);
}

/*
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
*/
}

synchronized类比知识点:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count– 代码

图解

image-20230108230243755

思考

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。

为了加深理解,请思考下面的问题:

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?– 原子性 直接锁住整个 for 循环
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?– 不行,锁对象不同
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?– 不行,锁对象没到位

优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test2")
public class Test2 {
// 使用 OOP 思想创建 lock 来保护资源
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");

t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}

class Room {
private int counter = 0;

public synchronized void increment() {
counter++;
}

public synchronized void decrement() {
counter--;
}

public synchronized int getCounter() {
/*synchronized (this) {
return counter;
}*/
return counter;
}
}

synchronized八锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;


@Slf4j(topic = "c.Test3")
public class Test3 {
// 线程8锁训练 --1
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
}
}

@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test4")
public class Test4 {
// 线程8锁训练 --2
public static void main(String[] args) {
Number2 n1 = new Number2();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
}
}

@Slf4j(topic = "c.Number2")
class Number2{
public synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test5")
public class Test5 {
// 线程8锁训练 --3
public static void main(String[] args) {
Number3 n3 = new Number3();
new Thread(() -> {
log.debug("begin");
n3.a();
}).start();

new Thread(() -> {
log.debug("begin");
n3.b();
}).start();

new Thread(() -> {
log.debug("begin");
n3.c();
}).start();
}
}

@Slf4j(topic = "c.Number3")
class Number3{
public synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test6")
public class Test6 {
// 线程8锁训练 --4
public static void main(String[] args) {
Number2 n1 = new Number2();
Number2 n2 = new Number2();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();

new Thread(() -> {
log.debug("begin");
n2.b();
}).start();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test7")
public class Test7 {
// 线程8锁训练 --5
public static void main(String[] args) {
Number4 n4 = new Number4();
new Thread(() -> {
n4.a();
}).start();
new Thread(() -> {
n4.b();
}).start();
}
}

@Slf4j(topic = "c.Number4")
class Number4 {
// 标注在静态方法上,锁住的是 Number4 这个类对象
public static synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
// 标注在非静态方法上,仅锁住这个对象
public synchronized void b() {
log.debug("2");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j(topic = "c.Test8")
public class Test8 {
// 线程8锁训练 --6
public static void main(String[] args) {
Number5 n5 = new Number5();
new Thread(() -> {
n5.a();
}).start();
new Thread(() -> {
n5.b();
}).start();
}
}

@Slf4j(topic = "c.Number5")
class Number5 {
// 标注在静态方法上,锁住的是 Number4 这个类对象
public static synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
// 标注在静态方法上
public static synchronized void b() {
log.debug("2");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test9")
public class Test9 {
// 线程8锁训练 --7
public static void main(String[] args) {
Number4 n1 = new Number4();
Number4 n2 = new Number4();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test10")
public class Test10 {
// 线程8锁训练 --8
public static void main(String[] args) {
Number5 n1 = new Number5();
Number5 n2 = new Number5();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
}

变量线程安全分析

成员变量与静态变量的线程安全

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量的线程安全

  • 局部基本类型变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

基础变量

1
2
3
4
5
public static void test1() {
int i = 10;
i++;
}
// 是线程安全的

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I

图解

image-20230109003128651

局部引用变量的不同

先看一个成员变量的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Slf4j(topic = "c.Test11")
public class Test11 {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}

class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}

出现一种情况,如果线程2 还未 add,线程1 remove 就会报错:

1
2
3
4
5
6
7
8
9
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35)
at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26)
at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)

// 共享同一个对象中的 list 成员变量,存在线程安全问题

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同

图解

image-20230109003813888

将 list 修改为局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}

就能解决上述问题,解决线程安全问题

分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

图解

image-20230109004021199

局部变量不暴露引用是线程安全的,局部变量暴力引用 –> 方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况1:有其它线程调用 method2 和 method3 –不会存在线程安全问题
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法 –存在线程安全问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
// 不能控制子类内部的线程安全,所以在 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法后,ThreadSafe 类并不是线程安全的

从这个例子可以看出方法的修饰符 private 或 final 一定意义上提供【安全】的所在,请体会开闭原则中的【闭】

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

1
2
3
4
5
6
7
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();

注意

  • 它们的每个方法是原子的
  • 但它们多个方法的组合不是原子的,见后面分析

线程安全类方法的组合

分析下面代码是否线程安全?

1
2
3
4
5
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}

图解

image-20230109005559478

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

1
2
3
4
5
6
7
8
9
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}

如果想增加一个增加的方法呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}

public Immutable add(int v){
return new Immutable(this.value + v);
}
}

TODO 不可变类的设计

案例分析

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyServlet extends HttpServlet {
// 是否安全?不是
Map<String,Object> map = new HashMap<>();
// 是否安全?是
String S1 = "...";
// 是否安全?是
final String S2 = "...";
// 是否安全?不是 Date 引用变量,对象中内容并不确保原子性,且为可变的
Date D1 = new Date();
// 是否安全?不是 --引用值不可变,堆中对象存储的内容可变
final Date D2 = new Date();

public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyServlet extends HttpServlet {
// 是否安全?不是
private UserService userService = new UserServiceImpl();

public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;

public void update() {
// ...
count++;
}
}

例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class MyAspect {
// 是否安全?不是,单例中成员方法与变量都要共享
private long start = 0L;

@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}

@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}

例4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyServlet extends HttpServlet {
// 是否安全 是
private UserService userService = new UserServiceImpl();

public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全 是
private UserDao userDao = new UserDaoImpl();

public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全 是 --无成员变量
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}

例5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();

public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();

public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全 否 成员变量被共享 --尽量做成线程私有的局部变量而不是成员变量
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}

例6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();

public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全 是 由于 UserDao userDao = new UserDaoImpl(); 作为局部变量不断创建,确保了线程安全
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}

例7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Test {

public void bar() {
// 是否安全 否 abstract类的 public 方法,会被重写
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}

public abstract foo(SimpleDateFormat sdf);

public static void main(String[] args) {
new Test().bar();
}
}

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法 –想方法不被暴露就设置成private或者final修饰,增强安全性

1
2
3
4
5
6
7
8
9
10
11
12
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}

比较 JDK 中 String 类的实现 –为什么 String 使用 final 修饰,因为如果不使用 final 修饰,就可能会有 String 的子类覆盖掉 String 的某个方法,导致线程不安全问题

例8:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int j = 0; j < 2; j++) {
Thread thread = new Thread(() -> {
for (int k = 0; k < 5000; k++) {
synchronized (i) {
i++;
}
}
}, "" + j);
list.add(thread);
}
list.stream().forEach(t -> t.start());
list.stream().forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
log.debug("{}", i);
}

线程安全案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

@Slf4j(topic = "c.Test11")
public class Test11 {
// 线程安全练习1 --卖票买票问题
public static void main(String[] args) throws InterruptedException {
// 模拟多人买票
TicketWindow window = new TicketWindow(1000);

// 所有线程的集合
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
// 买票
int amount = window.sell(random(5));
// 统计买票数
amountList.add(amount);
});
threadList.add(thread);
thread.start();
}

for (Thread thread : threadList) {
thread.join();
}

// 统计卖出的票数和剩余票数
log.debug("余票:{}",window.getCount());
log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum());
}

// Random 为线程安全
static Random random = new Random();

// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 1;
}
}

// 售票窗口
class TicketWindow {
private int count;

public TicketWindow(int count) {
this.count = count;
}

// 获取余票数量
public int getCount() {
return count;
}

// 售票 --使用 synchronized 锁,加载 this 对象上,确保多线程操作 count 资源时保证线程安全
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
1
2
# 测试脚本
for /L %n in (1,1,10) do java -cp ".;C:\Users\manyh\.m2\repository\ch\qos\logback\logbackclassic\1.2.3\logback-classic-1.2.3.jar;C:\Users\manyh\.m2\repository\ch\qos\logback\logbackcore\1.2.3\logback-core-1.2.3.jar;C:\Users\manyh\.m2\repository\org\slf4j\slf4japi\1.7.25\slf4j-api-1.7.25.jar" cn.itcast.n4.exercise.ExerciseSell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package JUC.chapter_2_ThreadSafe;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

@Slf4j(topic = "c.Test12")
public class Test12 {
// 线程安全练习2 --转账问题
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}

// Random 为线程安全
static Random random = new Random();

// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}

}

// 账户
class Account {
private int money;

public Account(int money) {
this.money = money;
}

public int getMoney() {
return money;
}

public void setMoney(int money) {
this.money = money;
}

// 转账
public void transfer(Account target, int amount) {
synchronized(Account.class) {
// 如果将 synchronized 加在方法上,只能锁住当前对象的数据值,而由于转账操作牵扯到两个对象间的数据共享变化
// 通过 synchronized 只锁住当前对象就不能解决,只能通过加`大锁` synchronized 直接锁住 Account 账户类,就能解决线程不安全问题
// 但是引发的另一个问题,如果账户转账对象不一样,那么就会因为锁的力度太大而影响到,不同用户之间,不能同时转账,
// 即 即使是不同用户之间转账,也需要排队,类比食堂打菜问题,TODO优化 `大锁` 实现更小力度的锁,来解决不同用户之间同时转账的性能问题
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}

Monitor概念

Java对象头 –深入理解

以 32 位虚拟机为例

普通对象:

Integer 对象 Header 64bit->8byte Value(int ->4byte) –> 共12byte

int 4byte

内存敏感的情况下推荐使用 int 等基本类型,而不是 引用类型

image-20230109114507040
  • Mark Word 对象标记
  • Klass Work 类型指针 -> 指向堆内存中对象

数组对象:

image-20230109114541959

其中 Mark Word 结构为:

image-20230109114804679

64 位虚拟机 Mark Word

image-20230109114825997
  • hashcode –类的唯一标识
  • age –类的新生代特征,用于GC垃圾回收
  • biased_lock –偏向锁标识码
  • biased_lock后两位是,锁状态

参考资料:

what-is-in-java-object-header

Monitor原理

Monitor 被翻译为监视器管程

Monitor为 Java synchronized 锁底层原理

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

底层原理见 –Monitor操作系统层原理

synchronized锁优化原理

问题引入

故事角色

老王 - JVM

小南 - 线程

小女 - 线程

房间 - 对象

房间门上 - 防盗锁 - Monitor

房间门上 - 小南书包 - 轻量级锁

房间门上 - 刻上小南大名 - 偏向锁

批量重刻名 - 一个类的偏向锁撤销到达 20 阈值

不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。

但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?

小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。

后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。

于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。

同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字

后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包

Synchronized字节码原理

Synchronized锁优化原理

wait-notify解决方案

问题引入

  • 小南获得了锁
  • 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
  • 直到小M将烟送来,大叫一声 [你的烟到了] (调用 notify 方法)
  • 小南于是可以离开休息室,重新进入竞争锁的队列
wait-notify API

wait-notify API

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒,重新进入竞争锁的队列 EntryList
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒,重新进入竞争锁的队列 EntryList

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁 –成为Owner,才能调用这几个方法

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Slf4j(topic = "c.Test1")
public class Test1 {
// 测试 wait API
static final Object lock = new Object();
public static void main(String[] args) {
log.debug(ClassLayout.parseInstance(lock).toPrintableSimple());
synchronized (lock) {
log.debug(ClassLayout.parseInstance(lock).toPrintableSimple());
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}


@Slf4j(topic = "c.Test2")
public class Test2 {
// 测试 notify 与 notifyAll 的区别
final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {

new Thread(() -> {
synchronized (obj) {
log.debug("t1执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1其它代码....");
}
},"t1").start();

new Thread(() -> {
synchronized (obj) {
log.debug("t2执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t2其它代码....");
}
},"t2").start();

// 主线程两秒后执行
TimeUnit.MILLISECONDS.sleep(500);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 随机唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}


@Slf4j(topic = "c.Test3")
public class Test3 {
// 测试 wait(2000) 参数
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("t1执行....");
try {
obj.wait(2000); // 让线程在obj上等待2s,如果被 notify 就提前唤醒,没有就等2s后自己唤醒进入竞争队列
// obj.wait(2000, 2000); // 第二个参数是纳秒,但是并没有实现,因为纳秒的计算需要的算力太过于精确,而设置第二个参数也仅仅是让毫秒+1
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1其它代码....");
}
},"t1").start();
}
}
  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止
  • wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
wait-notify原理

wait-notify原理

sleep(long n) 对比 wait(long n)

sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制synchronized 配合使用,但 wait 需要 和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们的状态都是 TIMED_WAITING
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Slf4j(topic = "c.Test4")
public class Test4 {
// 测试对比 sleep 与 wait
/*
1. sleep 是 Thread 方法,而 wait 是 Object 的方法
2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用,
即成为对象 owner 后才能调用 wait
3. sleep 在睡眠的同时,如果加了 synchronized锁,是不会释放对象锁的,但 wait 在等待的时候会释放对象锁
4. sleep 与 wait 的状态都是 TIMED_WAITING
*/
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("t1获得锁");
try {
// Thread.sleep(20000);
obj.wait(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("t1重新唤醒");
}
}, "t1").start();

// 等待一秒后去争抢锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

synchronized (obj) {
log.debug("main获得锁");
// sleep 不能获取锁
// wait 可以获取锁
}
}
}
wait notify案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
@Slf4j(topic = "c.Test5")
public class Test5 {
// wait notify 应用案例 --案例1 错误案例,使用sleep使锁不释放,导致效率低
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟

public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();

for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}

sleep(1);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
// 如果使用 sleep 会导致不释放对象锁,此时就不能通过 锁 机制来开启 sleep 的线程
// synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
// }
}, "送烟的").start();
}
}


@Slf4j(topic = "c.Test6")
public class Test6 {
// wait notify 应用案例 --单个等待案例2 使用 wait notify 叫醒 wait 线程
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟

public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();

for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}

sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
}
}


@Slf4j(topic = "c.Test7")
public class Test7 {
// wait notify 应用案例 --多个等待案例3 notifyAll 虚假唤醒问题
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();

new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();

sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
room.notify(); // 随机唤醒一个线程 --错误唤醒/虚假唤醒
}
}, "送外卖的").start();
}
}


@Slf4j(topic = "c.Test8")
public class Test8 {
// wait notify 应用案例 --多个等待案例4 notifyAll 与 while 解决虚假唤醒问题
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();

new Thread(() -> {
synchronized (room) {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();

sleep(1);

new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}

Test5

  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加 synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

Test6

  • 解决了其它干活的线程阻塞的问题
  • 但如果有其它线程也在等待条件呢?

Test7

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 解决方法,改为 notifyAll

Test8

  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新 判断的机会了
  • 解决方法,用 while + wait,当条件不成立,再次 wait

总结

1
2
3
4
5
6
7
8
9
10
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}

park-unpark解决方案

park-unpark理论

park-unpark都是 LockSupport 类中的方法,用于 暂停当前线程恢复某个线程的运行

  • 先 park 再 unpark
  • 先 unpark 再 park –> 如果先 unpark 后,再 park 就会失效一次
1
2
3
4
5
// 暂停当前线程
LockSupport.park();

// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
park-unpark API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j(topic = "c.Test13")
public class Test13 {
// 测试 pack/unpack API使用
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
// park对应状态为 WAIT状态,unpark 在park前后调用都可以
// 每个线程底层都关联一个 parker 对象,是由c实现的,由三个部分组成
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();

sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
park-unpark底层原理

park-unpark原理

线程状态转换问题

理解线程状态转换过程

image-20230110002541500

解析 –存在线程 Thread t

  1. NEW –> RUNNABLE

    • 当调用 t.start() 方法时,由 NEW –> RUNNABLE
  2. RUNNABLE <–> WAITING

    t 线程用 synchronized(obj) 获取了对象锁

    • 调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
    • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
      • 竞争锁成功,t 线程从 WAITING –> RUNNABLE
      • 竞争锁失败,t 线程从 WAITING –> BLOCKED
  3. RUNNABLE <–> WAITING

    • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
      • 注意是当前线程在t 线程对象的监视器上等待
    • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
  4. RUNNABLE <–> WAITING

    • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
    • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
  5. RUNNABLE <–> TIMED_WAITING

    t 线程用 synchronized(obj) 获取了对象锁后

    • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING
    • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
      • 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE
      • 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED
  6. RUNNABLE <–> TIMED_WAITING

    • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING
      • 注意是当前线程在t 线程对象的监视器上等待
    • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
  7. RUNNABLE <–> TIMED_WAITING

    • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
    • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
  8. RUNNABLE <–> TIMED_WAITING

    • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE –> TIMED_WAITING
    • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
  9. RUNNABLE <–> BLOCKED

    • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
    • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
  10. RUNNABLE <–> TERMINATED

    • 当前线程所有代码运行完毕,进入 TERMINATED

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Slf4j(topic = "c.Test14")
public class Test14 {
// 线程转换状态
final static Object obj = new Object();

public static void main(String[] args) {

new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();

new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();

// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}

多锁-死锁-活锁-饥饿等活跃性问题

多把锁

多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。

现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

解决方法是准备多个房间(多个对象锁)

问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Test15 {
// 多把锁应用 --细粒度锁
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}

class BigRoom {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}

优化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Slf4j(topic = "c.Test15")
public class Test15 {
// 多把锁应用 --细粒度锁
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}

@Slf4j(topic = "c.BigRoom")
class BigRoom {

private final Object studyRoom = new Object();

private final Object bedRoom = new Object();

public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}

public void study() {
synchronized (studyRoom) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
活跃性
死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁 例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Slf4j(topic = "c.Test16")
public class Test16 {
// 死锁案例
public static void main(String[] args) {
test1();
}

private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");

Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}

解决死锁问题

  1. 定位死锁

    • 使用 jps 定位进程 id,再用 jstack 定位死锁:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      cmd > jps
      Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
      12320 Jps
      22816 KotlinCompileDaemon
      33200 TestDeadLock // JVM 进程
      11508 Main
      28468 Launcher

      cmd > jstack 33200
      Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
      2018-12-29 05:51:40
      Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode):
      "DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition
      [0x0000000000000000]
      java.lang.Thread.State: RUNNABLE
      "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry
      [0x000000001f54f000]
      java.lang.Thread.State: BLOCKED (on object monitor)
      at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
      - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
      - locked <0x000000076b5bf1d0> (a java.lang.Object)
      at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:745)
      "Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry
      [0x000000001f44f000]
      java.lang.Thread.State: BLOCKED (on object monitor)
      at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
      - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
      - locked <0x000000076b5bf1c0> (a java.lang.Object)
      at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:745)

      // 略去部分输出
      Found one Java-level deadlock:
      =============================
      "Thread-1":
      waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
      which is held by "Thread-0"
      "Thread-0":
      waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
      which is held by "Thread-1"
      Java stack information for the threads listed above:
      ===================================================
      "Thread-1":
      at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
      - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
      - locked <0x000000076b5bf1d0> (a java.lang.Object)
      at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:745)
      "Thread-0":
      at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
      - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
      - locked <0x000000076b5bf1c0> (a java.lang.Object)
      at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:745)
      Found 1 deadlock.
    • 检测死锁 –使用 jconsole工具

      image-20230110013838202
  2. 解决死锁问题 –破坏死锁形成条件

    • 避免死锁,要注意加锁顺序
    • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

哲学家就餐问题

image-20230110013414843

问题概述

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待

双重 synchronized 锁导致 死锁问题

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}


class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}

@Override
public void run() {
while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}


@Slf4j(topic = "c.Test21")
public class Test21 {
// ReentrantLock --应用 锁超时机制解决 哲学家就餐问题
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}

执行不多会,就执行不下去了 –死锁问题

使用 jconsole 检测死锁,发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
- 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁饥饿者两种情况

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j(topic = "c.Test17")
public class Test17 {
// 活锁案例
// 解决方案,可以通过增加随机的睡眠时间来减少活锁的产生
static volatile int count = 10;
static final Object lock = new Object();

public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}

解决方案

  • 通过执行时间交错来解决活锁问题
  • 通过增加随机睡眠时间来减少活锁的产生
饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题

下面我讲一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

image-20230110015140701

分析

  1. 线程1 获得了 对象A 的锁
  2. 线程2 获得了 对象B 的锁
  3. 线程1 尝试获取 对象B 的锁
  4. 线程2 尝试获取 对象A 的锁

–> 最终导致 死锁 问题

使用 顺序加锁 的解决方案

分析

  1. 线程1 获得了 对象A 的锁
  2. 线程2 尝试获得了 对象A 的锁 –> 阻塞
  3. 线程1 获取 对象B 的锁 –> 完成任务释放锁
  4. 线程2 尝试获取 对象A 的锁

–> 极大可能产生 饥饿 问题 –没有产生死锁,但是由于解决死锁问题的分配策略,部分线程获取锁的频率太低,导致一致在阻塞,从而造成饥饿问题

ReentrantLock解决方案

ReentrantLock理论

–可重入锁

ReentrantLock特点 –对比synchronized锁

  • 中断 –synchronized锁不能中断,加上synchronized锁后并不能在其他线程使用语法中断锁
  • 可以设置超时时间 –synchronized锁,发生阻塞等待时,只能一直等待,并不能设置等待时间
  • 可以设置为公平锁 –synchronized锁不能设置公平锁,而ReentrantLock可以(公平锁 -> 防止线程饥饿问题,在阻塞队列中排序,FIFO)
  • 支持多个条件变量 –synchronized锁,只能包含一个 WaitSet,ReentrantLock支持多个 WaitSet

与 synchronized 一样,都支持可重入 –可重入(可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住)

ReentrantLock语法

1
2
3
4
5
6
7
8
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入

可重入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Slf4j(topic = "c.Test18")
public class Test18 {
// ReentrantLock --可重入锁,ReentrantLock是JUC并发包下的一个重要类,
// 与 synchronized锁 对比学习,所以提前讲
// 特性1 --可重入锁 synchronized锁 与 ReentrantLock锁 都是可重入锁 TODO 锁粗化
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.debug("enter main");
m1();
} finally {
lock.unlock();
}
}

public static void m1() {
lock.lock();
try {
log.debug("enter m1");
m2();
} finally {
lock.unlock();
}
}

public static void m2() {
lock.lock();
try {
log.debug("enter m2");
} finally {
lock.unlock();
}
}
}
可打断

可打断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Slf4j(topic = "c.Test19")
public class Test19 {
// ReentrantLock 特性2 --可打断
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争,就获得 lock 对象锁
// 如果有竞争就进入阻塞队列,但由于其可打断性质,可以被其他线程的 interrupt 方法打断
log.debug("try to obtain lock");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("not lock");
return;
}
try {
log.debug("obtain lock");
} finally {
lock.unlock();
}
}, "t1");

// 产生锁竞争,上锁后启动 t1 线程,t1 线程进入阻塞队列
lock.lock();
// t1 线程进入阻塞队列
t1.start();

sleep(1);
// 打断阻塞队列中等待的 t1 线程
log.debug("interrupt t1");
t1.interrupt();
}

/*D:\JAVA\jdk1.8_0_211\bin\java.exe -javaagent:D:\JetBrains\ToolboxDownload\apps\IDEA-U\ch-0\223.7571.182\lib\idea_rt.jar=6251:D:\JetBrains\ToolboxDownload\apps\IDEA-U\ch-0\223.7571.182\bin -Dfile.encoding=UTF-8 -classpath D:\JAVA\jdk1.8_0_211\jre\lib\charsets.jar;D:\JAVA\jdk1.8_0_211\jre\lib\deploy.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\access-bridge-64.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\cldrdata.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\dnsns.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\jaccess.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\jfxrt.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\localedata.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\nashorn.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\sunec.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\sunjce_provider.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\sunmscapi.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\sunpkcs11.jar;D:\JAVA\jdk1.8_0_211\jre\lib\ext\zipfs.jar;D:\JAVA\jdk1.8_0_211\jre\lib\javaws.jar;D:\JAVA\jdk1.8_0_211\jre\lib\jce.jar;D:\JAVA\jdk1.8_0_211\jre\lib\jfr.jar;D:\JAVA\jdk1.8_0_211\jre\lib\jfxswt.jar;D:\JAVA\jdk1.8_0_211\jre\lib\jsse.jar;D:\JAVA\jdk1.8_0_211\jre\lib\management-agent.jar;D:\JAVA\jdk1.8_0_211\jre\lib\plugin.jar;D:\JAVA\jdk1.8_0_211\jre\lib\resources.jar;D:\JAVA\jdk1.8_0_211\jre\lib\rt.jar;E:\Todo\note\SoftwareDocument\Java_Note\JUC\Code\JUC-Code\target\classes;D:\maven-repository\org\projectlombok\lombok\1.18.24\lombok-1.18.24.jar;D:\maven-repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\maven-repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\maven-repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\maven-repository\org\openjdk\jol\jol-core\0.16\jol-core-0.16.jar JUC.chapter_4_Monitor.Test19
02:18:56.846 c.Test19 [t1] - try to obtain lock
02:18:57.854 c.Test19 [main] - interrupt t1
02:18:57.855 c.Test19 [t1] - not lock
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at JUC.chapter_4_Monitor.Test19.lambda$main$0(Test19.java:21)
at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0*/
}

注意

如果是不可中断模式 –> lock.lockInterruptibly() 而是使用 lock.lock(),那么即使使用了 interrupt 也不会让等待中断

锁超时

锁超时

一定程度上避免线程无限等待 –> 从而避免死锁问题

  • 立刻失败 lock.tryLock()
  • 超时失败 lock.tryLock(1, TimeUnit.SECONDS)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j(topic = "c.Test20")
public class Test20 {
// ReentrantLock 特性3 --锁超时
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获得锁");
try {
if (! lock.tryLock(2, TimeUnit.SECONDS)) {
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取不到锁");
return;
}
try {
log.debug("获得到锁");
} finally {
lock.unlock();
}
}, "t1");

lock.lock();
log.debug("获得到锁");
t1.start();
sleep(1);
log.debug("释放了锁");
lock.unlock();
}
}

tryLock 解决哲学家就餐问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Slf4j(topic = "c.Test21")
public class Test21 {
// ReentrantLock --应用 锁超时机制解决 哲学家就餐问题
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;

public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}

@Override
public void run() {
while (true) {
// 尝试获得左手筷子
if(left.tryLock()) {
try {
// 尝试获得右手筷子
if(right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock(); // 释放自己手里的筷子
}
}
}
}

Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}

class Chopstick extends ReentrantLock {
String name;

public Chopstick(String name) {
this.name = name;
}

@Override
public String toString() {
return "筷子{" + name + '}';
}
}
公平锁

公平锁

ReentrantLock 默认是不公平的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ReentrantLock lock = new ReentrantLock(false); // 默认是false --不公平的
lock.lock();
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " start...");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "强行插入").start();
lock.unlock();

强行插入,有机会在中间输出

注意:该实验不一定总能复现

–> 改为公平锁后

强行插入,总是在最后输出

公平锁一般没有必要,会降低并发度,后面分析原理时会讲解

多条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被 signal() 唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Slf4j(topic = "c.Test22")
public class Test22 {
// ReentrantLock --多条件变量应用
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
// 等外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();

public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();

new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小女").start();

sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();

sleep(1);

new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
ReentrantLock底层原理

ReentrantLock原理

chapter-5 Memory内存模型详解

Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性,这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题

内容概述

  • 原子性
  • 可见性
  • 有序性

JMM内存模型

JMM 即 Java Memory Model,它定义了主存工作内存抽象概念,底层对应着 CPU 寄存器缓存硬件内存CPU 指令优化等。

JMM 体现在以下几个方面:

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

可见性分析与解决方案

可见性问题

代码详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Slf4j(topic = "c.Test1")
public class Test1 {
// Java内存模型分析 不可见性

// 解决不可见性方案1 volatile关键字
// volatile关键字修饰后,避免线程从自己工作内存中的高速缓存中读取变量,而是从主存中获取他的值
// volatile 只能修饰成员变量和静态成员变量
// volatile static boolean run = true;
static boolean run = true;

// 锁对象
final static Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();

sleep(1);
log.debug("stop t1");
run = false; // 线程t不会如预想的停下来
}

// 解决不可见性方案2 synchronized 同步锁机制
public static void method2() {
Thread t = new Thread(() -> {
while (true) {
// ....
synchronized (lock) {
if (!run) {
break;
}
}
}
});
t.start();

sleep(1);
log.debug("stop t1");
synchronized (lock) {
run = false;
}
}
}

分析

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存
  2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
  3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

图解分析

image-20230110121004970 image-20230110121029094 image-20230110121050058

解决方法

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

原子性分析与解决方案

前面例子体现的实际就是可见性,它保证的是在多个线程之间一个线程对 volatile 变量的修改对另一个线程可见不能保证原子性仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的

1
2
3
4
5
6
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i– ,volatile只能保证看到最新值,不能解决指令交错

1
2
3
4
5
6
7
8
9
10
11
// 假设i的初始值为0
getstatic i // 线程2-获取静态变量i的值 线程内i=0

getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

注意

  • synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低
  • 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?

故而在操作代码块的时候不仅需要保证代码块内变量的可见性,还应该确保代码块执行的原子性

有序性分析与解决方案

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

1
2
3
4
5
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

1
2
i = ...;
j = ...;

也可以是

1
2
j = ...;
i = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧

详见指令级并行原理

指令有序性问题引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}

分析

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种

有同学这么分析,存在以下情况:

  1. 线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  2. 线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
  3. 线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

但结果还有可能是 0

这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:

借助 java 并发压测工具 jcstress

1
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -DartifactId=ordering -Dversion=1.0

创建 maven 项目,提供如下测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
int num = 0;
boolean ready = false;
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}

执行

1
2
mvn clean install
java -jar target/jcstress.jar

会输出我们感兴趣的结果,摘录其中一次结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.

2 matching test results.
[OK] test.ConcurrencyTest
(JVM args: [-XX:-TieredCompilation])
Observed state Occurrences Expectation Interpretation
0 1,729 ACCEPTABLE_INTERESTING !!!!
1 42,617,915 ACCEPTABLE ok
4 5,146,627 ACCEPTABLE ok
[OK] test.ConcurrencyTest
(JVM args: [])
Observed state Occurrences Expectation Interpretation
0 1,652 ACCEPTABLE_INTERESTING !!!!
1 46,460,657 ACCEPTABLE ok
4 4,571,072 ACCEPTABLE ok

可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了

解决方案

volatile 修饰的变量,可以禁用指令重排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true; // 只在 ready 上加 volatile 而 num 上没有加,volatile可以防止 ready 之前的代码被指令重排序,本质是加了一个写屏障
}
}

结果

1
2
3
4
*** INTERESTING tests
Some interesting behaviors observed. This is for the plain curiosity.

0 matching test results.

volatile原理

volatile原理

happens-before规则

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见 –synchronized
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int x;
static Object m = new Object();

new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();

new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
1
2
3
4
5
6
7
8
9
volatile static int x;

new Thread(()->{
x = 10;
},"t1").start();

new Thread(()->{
System.out.println(x);
},"t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
1
2
3
4
5
6
7
static int x;

x = 10;

new Thread(()->{
System.out.println(x);
},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
1
2
3
4
5
6
7
8
9
static int x;

Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();

t1.join();
System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int x;

public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();

new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();

while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
1
2
3
4
5
6
7
8
9
10
11
12
volatile static int x;
static int y;

new Thread(()->{
y = 10;
x = 20;
},"t1").start();

new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start();

chapter-6 无锁模型详解

无锁并发 -> 使用乐观锁(非阻塞锁)

内容概述

  • CAS 与 volatile
  • 原子整数
  • 原子引用
  • 原子累加器
  • Unsafe类

线程安全问题引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Slf4j(topic = "c.Test1")
public class Test1 {
// 取款不安全问题
// 解决方案1 --锁机制解决 synchronized ReentrantLock
// 解决方案2 --无锁实现 AtomicInteger 实现
public static void main(String[] args) {
Account account = new AccountCas(10000);
Account.demo(account);
}
}

class AccountCas implements Account {
private AtomicInteger balance;

public AccountCas(int balance) {
this.balance = new AtomicInteger(balance);
}

@Override
public Integer getBalance() {
return balance.get();
}

@Override
public void withdraw(Integer amount) {
/*while(true) {
// 获取余额的最新值
int prev = balance.get();
// 要修改的余额
int next = prev - amount;
// 真正修改
if(balance.compareAndSet(prev, next)) {
break;
}
}*/
balance.getAndAdd(-1 * amount);
}
}

class AccountUnsafe implements Account {

private Integer balance;

public AccountUnsafe(Integer balance) {
this.balance = balance;
}

@Override
public Integer getBalance() {
synchronized (this) {
return this.balance;
}
}

@Override
public void withdraw(Integer amount) {
synchronized (this) {
this.balance -= amount;
}
}
}

不安全的原因

withdraw 方法

1
2
3
public void withdraw(Integer amount) {
balance -= amount;
}

对应的字节码

1
2
3
4
5
6
7
8
9
ALOAD 0 // <- this
ALOAD 0
GETFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // <- this.balance
INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱
ALOAD 1 // <- amount
INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱
ISUB // 减法
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; // 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // -> this.balance

多线程执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ALOAD 0 // thread-0 <- this
ALOAD 0
GETFIELD cn/itcast/AccountUnsafe.balance // thread-0 <- this.balance
INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱
ALOAD 1 // thread-0 <- amount
INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱
ISUB // thread-0 减法
INVOKESTATIC java/lang/Integer.valueOf // thread-0 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance // thread-0 -> this.balance

ALOAD 0 // thread-1 <- this
ALOAD 0
GETFIELD cn/itcast/AccountUnsafe.balance // thread-1 <- this.balance
INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱
ALOAD 1 // thread-1 <- amount
INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱
ISUB // thread-1 减法
INVOKESTATIC java/lang/Integer.valueOf // thread-1 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance // thread-1 -> this.balance
  • 单核的指令交错
  • 多核的指令交错

CAS与volatile

前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void withdraw(Integer amount) {
while(true) {
// 需要不断尝试,直到成功为止
while (true) {
// 比如拿到了旧值 1000
int prev = balance.get();
// 在这个基础上 1000-10 = 990
int next = prev - amount;
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
- 不一致了,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 990
那么本线程的这次 990 就作废了,进入 while 下次循环重试
- 一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}

其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。

CAS

CAS

image-20230110190528461

注意

其实 CAS 的底层lock cmpxchg 指令(X86 架构 –CPU指令级别),在==单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性==。

  • 在==多核状态==下,某个核执行到带 lock 的指令时,CPU 会让==总线锁住==,当这个核把此指令执行完毕,再开启总线。这个过程中==不会被线程的调度机制所打断==,保证了多个线程对内存操作的准确性,是原子的。

细节分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Slf4j
public class SlowMotion {
public static void main(String[] args) {
AtomicInteger balance = new AtomicInteger(10000);
int mainPrev = balance.get();
log.debug("try get {}", mainPrev);
new Thread(() -> {
sleep(1000);
int prev = balance.get();
balance.compareAndSet(prev, 9000); // 打断点分析
log.debug(balance.toString());
}, "t1").start();
sleep(2000);
log.debug("try set 8000...");
boolean isSuccess = balance.compareAndSet(mainPrev, 8000);
log.debug("is success ? {}", isSuccess);
if(!isSuccess){
mainPrev = balance.get();
log.debug("try set 8000...");
isSuccess = balance.compareAndSet(mainPrev, 8000);
log.debug("is success ? {}", isSuccess);
}
}
private static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出结果

1
2
3
4
5
6
2019-10-13 11:28:37.134 [main] try get 10000
2019-10-13 11:28:38.154 [t1] 9000
2019-10-13 11:28:39.154 [main] try set 8000...
2019-10-13 11:28:39.154 [main] is success ? false
2019-10-13 11:28:39.154 [main] try set 8000...
2019-10-13 11:28:39.154 [main] is success ? true
volatile

volatile

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

注意

volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

效率分析

为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
  • 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
image-20230110223012536
CAS特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

原子整数

J.U.C 并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
AtomicInteger i = new AtomicInteger(0);

// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());

// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());

// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());

// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());

// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));

// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));

// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));

// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));

// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));

// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));


@Slf4j(topic = "c.Test2")
public class Test2 {
// 并发工具包 无锁工具类 AtomicAPI
// AtomicInteger
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);

System.out.println(i.incrementAndGet()); // ++i 1
System.out.println(i.getAndIncrement()); // i++ 2

System.out.println(i.getAndAdd(5)); // 2 , 7
System.out.println(i.addAndGet(5)); // 12, 12

System.out.println("--------------------");

// 自定义原子运算
AtomicInteger j = new AtomicInteger(2); // update set get
System.out.println(j.updateAndGet(x -> x * 10));
System.out.println(j.getAndUpdate(x -> x * 10));
System.out.println(j.get());
}
}


public class Test3 {
// updateAndGet 原理
public static void main(String[] args) {
}

/**
* @Description: updateAndGet原理实现
* @param i
* @param operator 传入一个函数接口
* @return: int
* @Author: Dracula
* @Date: 2022/12/29 3:12
*/
public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
while (true) {
int prev = i.get();
// 调用函数接口作为运算过程
int next = operator.applyAsInt(prev);
if (i.compareAndSet(prev, next)) {
return next;
}
}
}
}

原子引用

原子引用类型的作用

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference
原子引用实现

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface DecimalAccount {
// 获取余额
BigDecimal getBalance();
// 取款
void withdraw(BigDecimal amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}

试着提供不同的 DecimalAccount 实现,实现安全的取款操作

不安全实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DecimalAccountUnsafe implements DecimalAccount {
BigDecimal balance;
public DecimalAccountUnsafe(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
}

安全实现-锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DecimalAccountSafeLock implements DecimalAccount {
private final Object lock = new Object();
BigDecimal balance;
public DecimalAccountSafeLock(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
synchronized (lock) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
}
}

安全实现-CAS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class DecimalAccountSafeCas implements DecimalAccount {
AtomicReference<BigDecimal> ref;
public DecimalAccountSafeCas(BigDecimal balance) {
ref = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return ref.get();
}
@Override
public void withdraw(BigDecimal amount) {
while (true) {
BigDecimal prev = ref.get();
BigDecimal next = prev.subtract(amount);
if (ref.compareAndSet(prev, next)) {
break;
}
}
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Test4 {
// 原子参数类型 AtomicReference --原子引用
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
}
}

class DecimalAccountCas implements DecimalAccount {
private AtomicReference<BigDecimal> balance;

public DecimalAccountCas(BigDecimal balance) {
// this.balance = balance;
this.balance = new AtomicReference<>(balance);
}

@Override
public BigDecimal getBalance() {
return balance.get();
}

@Override
public void withdraw(BigDecimal amount) {
while(true) {
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}

interface DecimalAccount {
// 获取余额
BigDecimal getBalance();

// 取款
void withdraw(BigDecimal amount);

/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}
ABA问题

问题引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:

只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号

解决方案1 AtomicStampedReference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();
}

解决方案2 AtomicMarkableReference

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference

image-20230111011912294

AtomicMarkableReference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class GarbageBag {
String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return super.toString() + " " + desc;
}
}

@Slf4j
public class TestABAAtomicMarkableReference {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.debug("主线程 start...");
GarbageBag prev = ref.getReference();
log.debug(prev.toString());
new Thread(() -> {
log.debug("打扫卫生的线程 start...");
bag.setDesc("空垃圾袋");
while (!ref.compareAndSet(bag, bag, true, false)) {}
log.debug(bag.toString());
}).start();
Thread.sleep(1000);
log.debug("主线程想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(ref.getReference().toString());
}
}

解决方案代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Slf4j(topic = "c.Test5")
public class Test5 {
// ABA问题,A->B->A->B->A 在多线程情况下如何判断是哪个更改的,更改了多少次。如何感知变量的变化 --引出版本号问题
// 使用 AtomicStampedReference 解决问题
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
// 这个共享变量是否被其他线程修改过
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
sleep(1);
// 尝试改为 C 发现版本号已经被更改,版本号冲突,更改失败
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
log.debug(ref.getReference());
}

private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Slf4j(topic = "c.Test6")
public class Test6 {
// 原子引用 AtomicMarkableReference 通过mark简化版本号
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);

log.debug("start...");
GarbageBag prev = ref.getReference();
log.debug(prev.toString());

new Thread(() -> {
log.debug("start...");
bag.setDesc("空垃圾袋");
ref.compareAndSet(bag, bag, true, false);
log.debug(bag.toString());
},"保洁阿姨").start();

sleep(1);
log.debug("想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(ref.getReference().toString());
}
}

class GarbageBag {
String desc;

public GarbageBag(String desc) {
this.desc = desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

@Override
public String toString() {
return super.toString() + " " + desc;
}
}

原子数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Slf4j(topic = "c.Test7")
public class Test7 {
// 原子数组工具
// AtomicIntegerArray
// AtomicLongArray
// AtomicReferenceArray
public static void main(String[] args) {
demo(
()->new int[10],
array ->array.length,
(array, index) -> array[index]++,
array-> System.out.println(Arrays.toString(array))
); // 不安全数组

demo(
()-> new AtomicIntegerArray(10),
array -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
); // 安全数组
}

/**
description: 测试是否线程安全工具
参数1,提供数组、可以是线程不安全数组或线程安全数组
参数2,获取数组长度的方法
参数3,自增方法,回传 array, index
参数4,打印数组的方法
*/
// 函数式接口概述 -> lambda表达式
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->void
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer ) {
List<Thread> ts = new ArrayList<>();
T array = arraySupplier.get();
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j%length);
}
}));
}

ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等所有线程结束
printConsumer.accept(array);
}
}

字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常

1
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j(topic = "c.Test8")
public class Test8 {
// 字段更新器 --针对对象的某个属性(Field)进行原子操作
// 只能配合 volatile 修饰的字段使用,否则会出现异常
// Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type

// AtomicReferenceFieldUpdater 域 字段
// AtomicIntegerFieldUpdater
// AtomicLongFieldUpdater
public static void main(String[] args) {
Student stu = new Student();
System.out.println(stu);
AtomicReferenceFieldUpdater updater =
AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");

System.out.println(updater.compareAndSet(stu, null, "张三"));
System.out.println(stu);
}

}

class Student {
// 必须用 volatile 修饰
volatile String name;

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test5 {
private volatile int field;
public static void main(String[] args) {
AtomicIntegerFieldUpdater fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(Test5.class, "field");
Test5 test5 = new Test5();
fieldUpdater.compareAndSet(test5, 0, 10);
// 修改成功 field = 10
System.out.println(test5.field);
// 修改成功 field = 20
fieldUpdater.compareAndSet(test5, 10, 20);
System.out.println(test5.field);
// 修改失败 field = 20
fieldUpdater.compareAndSet(test5, 10, 30);
System.out.println(test5.field);
}
}

原子累加器

累加器性能比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
long start = System.nanoTime();
List<Thread> ts = new ArrayList<>();
// 4 个线程,每人累加 50 万
for (int i = 0; i < 40; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(adder + " cost:" + (end - start)/1000_000);
}

比较 AtomicLong 与 LongAdder

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < 5; i++) {
demo(() -> new LongAdder(), adder -> adder.increment());
}

for (int i = 0; i < 5; i++) {
demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}



测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class Test9 {
// 原子累加器
// jdk8后 新增了用来专门用于累加的类,用与多线程并发,相较于 AtomicLong 做累加性能上有卓越的提升
// DoubleAccumulator
// DoubleAdder
// LongAccumulator
// LongAdder
public static void main(String[] args) {
// 性能对比
demo(
() -> new AtomicLong(0),
(adder) -> adder.getAndIncrement()
);

demo(
() -> new LongAdder(),
adder -> adder.increment()
);
// 2000000 cost:34
// 2000000 cost:12

System.out.println("------------------------");

// 排除JIT优化性能测试
for (int i = 0; i < 5; i++) {
demo(
() -> new AtomicLong(0),
(adder) -> adder.getAndIncrement()
);
}

for (int i = 0; i < 5; i++) {
demo(
() -> new LongAdder(),
adder -> adder.increment()
);
}
}

// 测试方法
/*
() -> 结果 提供累加器对象
(参数) -> 执行累加操作
*/
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
List<Thread> ts = new ArrayList<>();
// 4 个线程,每人累加 50 万
for (int i = 0; i < 4; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
long start = System.nanoTime();
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

long end = System.nanoTime();
System.out.println(adder + " cost:" + (end - start) / 1000_000);
}
}

==性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。==

LongAdder源码

LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧

LongAdder 类有几个关键域 Field

1
2
3
4
5
6
7
8
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;

// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;

// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Slf4j(topic = "c.Test10")
public class Test10 {
// LongAdder原理性能分析
// AtomicLong 存在一个累加单元Cell[0],在同时进行累加时,必然会导致竞争特别激烈,竞争激烈就需要不断地进行CAS操作
// 而 LongAdder 性能提升就在于,他设置多个累加单元Cell[0]...,减少竞争激烈的情况,最后汇总,从而减少了CAS操作,提高性能
public static void main(String[] args) {
LockCas lock = new LockCas();
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
sleep(1);
} finally {
lock.unlock();
}
}).start();

new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
} finally {
lock.unlock();
}
}).start();
}
}

// 手写 CAS 仅提供测试使用,不能用于生产实践,CAS锁通常实现在底层,不要在上层实现!!
@Slf4j(topic = "c.LockCas")
class LockCas {
// 0 没加锁
// 1 加锁
private AtomicInteger state = new AtomicInteger(0);

public void lock() {
while (true) {
if (state.compareAndSet(0, 1)) {
break;
}
}
}

public void unlock() {
log.debug("unlock...");
state.set(0);
}
}

LongAdder原理 –伪共享

LongAdder源码 –> 其中 Cell 即为累加单元

1
2
3
4
5
6
7
8
9
10
11
12
// 防止缓存行伪共享
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }

// 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
final boolean cas(long prev, long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
// 省略不重要代码
}

原理从缓存说起,缓存与内存的速度比较:

图解

image-20230111023354713
从 cpu 到 大约需要的时钟周期
寄存器 1 cycle (4GHz 的 CPU 约为0.25ns)
L1 3~4 cycle
L2 10~20 cycle
L3 40~45 cycle
内存 120~240 cycle
  • 因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率
  • 缓存以缓存行为单位每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
  • 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
  • CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
image-20230111023836520

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]

无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效

@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

image-20230111024015560

累加主要调用下面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void add(long x) {
// as 为累加单元数组
// b 为基础值
// x 为累加值
Cell[] as; long b, v; int m; Cell a;
// 进入 if 的两个条件
// 1. as 有值, 表示已经发生过竞争, 进入 if
// 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
if ((as = cells) != null || !casBase(b = base, b + x)) {
// uncontended 表示 cell 没有竞争
boolean uncontended = true;
if (
// as 还没有创建
as == null || (m = as.length - 1) < 0 ||
// 当前线程对应的 cell 还没有
(a = as[getProbe() & m]) == null ||
// cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
!(uncontended = a.cas(v = a.value, v + x))
) {
// 进入 cell 数组创建、cell 创建的流程
longAccumulate(x, null, uncontended);
}
}
}

add 流程图

image-20230111024138090
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
if ((h = getProbe()) == 0) {
// 初始化 probe
ThreadLocalRandom.current();
// h 对应新的 probe 值, 用来对应 cell
h = getProbe();
wasUncontended = true;
}
// collide 为 true 表示需要扩容
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
// 已经有了 cells
if ((as = cells) != null && (n = as.length) > 0) {
// 还没有 cell
if ((a = as[(n - 1) & h]) == null) {
// 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
// 成功则 break, 否则继续 continue 循环
}
// 有竞争, 改变线程对应的 cell 来重试 cas
else if (!wasUncontended)
wasUncontended = true;
// cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
// 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
else if (n >= NCPU || cells != as)
collide = false;
// 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
else if (!collide)
collide = true;
// 加锁
else if (cellsBusy == 0 && casCellsBusy()) {
// 加锁成功, 扩容
continue;
}
// 改变线程对应的 cell
h = advanceProbe(h);
}
// 还没有 cells, 尝试给 cellsBusy 加锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
// 成功则 break;
}
// 上两种情况失败, 尝试给 base 累加
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
}
}

longAccumulate 流程图

image-20230111024229034 image-20230111024246760

每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)

image-20230111024304858

获取最终结果通过 sum 方法

1
2
3
4
5
6
7
8
9
10
11
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}

Unsafe对象

Unsafe概述

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UnsafeAccessor {
static Unsafe unsafe;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
static Unsafe getUnsafe() {
return unsafe;
}
}

Unsafe CAS 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
class Student {
volatile int id;
volatile String name;
}

Unsafe unsafe = UnsafeAccessor.getUnsafe();
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
// 获得成员变量的偏移量
long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
Student student = new Student();
// 使用 cas 方法替换成员变量的值
UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student);

使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class AtomicData {
private volatile int data;
static final Unsafe unsafe;
static final long DATA_OFFSET;
static {
unsafe = UnsafeAccessor.getUnsafe();
try {
// data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public AtomicData(int data) {
this.data = data;
}
public void decrease(int amount) {
int oldValue;
while(true) {
// 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
oldValue = data;
// cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
return;
}
}
}
public int getData() {
return data;
}
}

Account 实现

1
2
3
4
5
6
7
8
9
10
11
Account.demo(new Account() {
AtomicData atomicData = new AtomicData(10000);
@Override
public Integer getBalance() {
return atomicData.getData();
}
@Override
public void withdraw(Integer amount) {
atomicData.decrease(amount);
}
});

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Slf4j(topic = "c.Test11")
public class Test11 {
// unsafe原理
// unsafe 对象提供底层操作内存、线程,Unsafe 对象不能直接调用,只能通过反射获得
// unsafe 字面意思并不是线程不安全的意思,而是 unsafe 类直接操作底层线程内存资源,不推荐编程人员直接使用
// 误用会导致程序不安全的发生
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 通过反射获得成员变量
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
// 私有变量 设置强制反射
theUnsafe.setAccessible(true);
// 获得 unsafe 对象
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

System.out.println(unsafe);

// unsafe对象CAS操作的相关方法
// 通过 unsafe 与 反射 对 Teacher 对象线程安全的改变其 field
// unsafe 底层通过内存偏移量来定位到field从而获取到对象
// 1. 获取域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));

Teacher t = new Teacher();
// 2. 执行 cas 操作
// 如果有其他线程对其field进行修改就会导致 cas 重试再次修改
unsafe.compareAndSwapInt(t, idOffset, 0, 1);
unsafe.compareAndSwapObject(t, nameOffset, null, "张三");

// 3. 验证
System.out.println(t);
}
}

@Data
class Teacher {
volatile int id;
volatile String name;
}

Unsafe 模拟手写 AtomicData 实现线程安全的原子整数 Account

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Test12 {
// 模拟手写 AtomicData 实现线程安全的原子整数 Account
public static void main(String[] args) {
Account.demo(new MyAtomicInteger(10000));
}
}

class MyAtomicInteger implements Account {
// 配合 cas 操作
private volatile int value;
private static final long valueOffset;
private static final Unsafe UNSAFE;
static {
// 使用工具方法获取类的field构建unsafe对象
UNSAFE = UnsafeAccessor.getUnsafe();
try {
// unsafe对象 需要知道 value field 相对于整个类 MyAtomicInteger 的偏移量,从而确定 field域的位置
valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}

public int getValue() {
return value;
}

// 扣钱
public void decrement(int amount) {
while(true) {
int prev = this.value;
int next = prev - amount;
if (UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)) {
break;
}
}
}

public MyAtomicInteger(int value) {
this.value = value;
}

@Override
public Integer getBalance() {
return getValue();
}

@Override
public void withdraw(Integer amount) {
decrement(amount);
}
}

==TODO Unsafe原理整理==

chapter-7 不可变类详解

内容概述

  • 不可变类的使用
  • 不可变类设计
  • 无状态类设计

日期转换问题

问题引入

下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的

1
2
3
4
5
6
7
8
9
10
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}

有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
19:10:40.859 [Thread-2] c.TestDateParse - {}
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at cn.itcast.n7.TestDateParse.lambda$test1$0(TestDateParse.java:18)
at java.lang.Thread.run(Thread.java:748)
19:10:40.859 [Thread-1] c.TestDateParse - {}
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at cn.itcast.n7.TestDateParse.lambda$test1$0(TestDateParse.java:18)
at java.lang.Thread.run(Thread.java:748)
19:10:40.857 [Thread-8] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-9] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-6] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-4] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-5] c.TestDateParse - Mon Apr 21 00:00:00 CST 178960645
19:10:40.857 [Thread-0] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-7] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951
19:10:40.857 [Thread-3] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951

解决方案1 –同步锁

这样虽能解决问题,但带来的是性能上的损失,并不算很好:

1
2
3
4
5
6
7
8
9
10
11
12
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 50; i++) {
new Thread(() -> {
synchronized (sdf) {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}
}).start();
}

解决方案2 –不可变类

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在 Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:

1
2
3
4
5
6
7
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
log.debug("{}", date);
}).start();
}

可以参考 DateTimeFormatter 的文档:

1
2
@implSpec
This class is immutable and thread-safe.

不可变对象,实际是另一种避免竞争的方式。

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@Slf4j(topic = "c.Test1")
public class Test1 {
// 可变类在多线程下的线程安全问题
public static void main(String[] args) {
test1();
}

// 可变类在多线程下运行异常问题
/*
Exception in thread "Thread-8"
Exception in thread "Thread-7"
Exception in thread "Thread-1"
Exception in thread "Thread-3"
Exception in thread "Thread-2"
Exception in thread "Thread-0"
Exception in thread "Thread-4"
Exception in thread "Thread-6"
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
*/
// 原理 --SimpleDateFormat类 内部状态 和 内部一些成员变量都是可变的,这就会导致在多个线程并发执行时出现线程安全问题
private static void test1() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}).start();
}
}

// 解决方一:synchronized锁 与 ReentrantLock锁 进行加锁解决 --影响性能
private static void solution1() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (sdf) {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}

// 解决方案二:使用不可变对象 DateTimeFormatter
// --jdk1.8提供,不可变对于在多线程下是一个线程安全的,没有线程能修改他的状态
private static void solution2() {
// DateTimeFormatter的创建不是调用构造方法,而是使用一个工厂方法
DateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
TemporalAccessor parse = stf.parse("1951-04-21");
log.debug("{}", parse);
}).start();
}
}
}

不可变设计

String 类也是不可变的,以它为例,说明一下不可变设计的要素

1
2
3
4
5
6
7
8
9
10
11
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0

// ...

}
Final的作用

Final的作用

String类、类中所有属性都是 final 的

  • 属性用 final 修饰保证了该属性是只读的,不能修改
  • 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
保护性拷贝

但使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 substring 为例:

1
2
3
4
5
6
7
8
9
10
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出了修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}

结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】

无状态

在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的

因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】

chapter-8 多线程应用工具

线程池解决方案

自定义线程池
image-20230112000407491

实现步骤:

步骤1:自定义拒绝策略接口

1
2
3
4
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}

步骤2:自定义任务队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class BlockingQueue<T> {
// 1. 任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2. 锁
private ReentrantLock lock = new ReentrantLock();
// 3. 生产者条件变量
private Condition fullWaitSet = lock.newCondition();
// 4. 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5. 容量
private int capcity;
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
// 带超时阻塞获取
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
// 将 timeout 统一转换为 纳秒
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
// 返回值是剩余时间
if (nanos <= 0) {
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// 阻塞获取
public T take() {
lock.lock();
try {
while (queue.isEmpty()) {
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// 阻塞添加
public void put(T task) {
lock.lock();
try {
while (queue.size() == capcity) {
try {
log.debug("等待加入任务队列 {} ...", task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
// 带超时时间阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capcity) {
try {
if(nanos <= 0) {
return false;
}
log.debug("等待加入任务队列 {} ...", task);
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
// 判断队列是否满
if(queue.size() == capcity) {
rejectPolicy.reject(this, task);
} else { // 有空闲
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
}
} finally {
lock.unlock();
}
}
}

步骤3:自定义线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务时的超时时间
private long timeout;
private TimeUnit timeUnit;
private RejectPolicy<Runnable> rejectPolicy;
// 执行任务
public void execute(Runnable task) {
// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
// 如果任务数超过 coreSize 时,加入任务队列暂存
synchronized (workers) {
if(workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("新增 worker{}, {}", worker, task);
workers.add(worker);
worker.start();
} else {
// taskQueue.put(task);
// 1) 死等
// 2) 带超时等待
// 3) 让调用者放弃任务执行
// 4) 让调用者抛出异常
// 5) 让调用者自己执行任务
taskQueue.tryPut(rejectPolicy, task);
}
}
}
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,
RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 执行任务
// 1) 当 task 不为空,执行任务
// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
// while(task != null || (task = taskQueue.take()) != null) {
while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
log.debug("正在执行...{}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
log.debug("worker 被移除{}", this);
workers.remove(this);
}
}
}
}

步骤4:测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1,
1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
// 1. 死等
// queue.put(task);
// 2) 带超时等待
// queue.offer(task, 1500, TimeUnit.MILLISECONDS);
// 3) 让调用者放弃任务执行
// log.debug("放弃{}", task);
// 4) 让调用者抛出异常
// throw new RuntimeException("任务执行失败 " + task);
// 5) 让调用者自己执行任务
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
@Slf4j(topic = "c.Test1")
public class Test1 {
// 线程池学习
// 自定义线程池
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1,
1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
// 1. 死等
// queue.put(task);
// 2) 带超时等待
// queue.offer(task, 1500, TimeUnit.MILLISECONDS);
// 3) 让调用者放弃任务执行
// log.debug("放弃{}", task);
// 4) 让调用者抛出异常
// throw new RuntimeException("任务执行失败 " + task);
// 5) 让调用者自己执行任务
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}
}

@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}

@Slf4j(topic = "c.ThreadPool")
class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;

// 线程集合
private HashSet<Worker> workers = new HashSet<>();

// 核心线程数
private int coreSize;

// 获取任务时的超时时间
private long timeout;

private TimeUnit timeUnit;

private RejectPolicy<Runnable> rejectPolicy;

// 执行任务
public void execute(Runnable task) {
// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
// 如果任务数超过 coreSize 时,加入任务队列暂存
synchronized (workers) {
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("新增 worker{}, {}", worker, task);
workers.add(worker);
worker.start();
} else {
// taskQueue.put(task);
// 1) 死等
// 2) 带超时等待
// 3) 让调用者放弃任务执行
// 4) 让调用者抛出异常
// 5) 让调用者自己执行任务
// 使用策略模式
taskQueue.tryPut(rejectPolicy, task);
}
}
}

public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.rejectPolicy = rejectPolicy;
}

class Worker extends Thread {
private Runnable task;

public Worker(Runnable task) {
this.task = task;
}

@Override
public void run() {
// 执行任务
// 1) 当 task 不为空,执行任务
// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
// while(task != null || (task = taskQueue.take()) != null) {
while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
log.debug("正在执行...{}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
log.debug("worker 被移除{}", this);
workers.remove(this);
}
}
}
}

@Slf4j(topic = "c.BlockingQueue")
class BlockingQueue<T> {
// 1. 任务队列
private Deque<T> queue = new ArrayDeque<>();

// 2. 锁
private ReentrantLock lock = new ReentrantLock();

// 3. 生产者条件变量
private Condition fullWaitSet = lock.newCondition();

// 4. 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();

// 5. 容量
private int capcity;

public BlockingQueue(int capcity) {
this.capcity = capcity;
}

// 带超时阻塞获取 TimeUnit 是jdk5中出现的便于时间单位的转换
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
// 将 timeout 统一转换为 纳秒
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
// 返回值是剩余时间
if (nanos <= 0) {
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}

// 阻塞获取
public T take() {
lock.lock();
try {
while (queue.isEmpty()) {
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}

// 阻塞添加
public void put(T task) {
lock.lock();
try {
while (queue.size() == capcity) {
try {
log.debug("等待加入任务队列 {} ...", task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}

// 带超时时间阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capcity) {
try {
if (nanos <= 0) {
return false;
}
log.debug("等待加入任务队列 {} ...", task);
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}

public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}

public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
// 判断队列是否满
if (queue.size() == capcity) {
rejectPolicy.reject(this, task);
} else { // 有空闲
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
}
} finally {
lock.unlock();
}
}
}
ThreadPoolExecutor
image-20230112004316681

线程池状态

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量

状态名 高 3 位 接收新任务 处理阻塞队列任务 说明
RUNNING 111 Y Y
SHUTDOWN 000 N Y 不会接收新任务,但会处理阻塞队列剩余任务
STOP 001 N N 会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING 010 - - 任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED 011 - - 终结状态

==从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING==

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

1
2
3
4
5
// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));

// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }

构造方法

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

分析:

  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字
  • handler 拒绝策略

工作方式:

image-20230112024920789

分析:

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
  • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。
  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。
  • 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现
    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
    • CallerRunsPolicy 让调用者运行任务
    • DiscardPolicy 放弃本次任务
    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
    • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
    • Netty 的实现,是创建一个新线程来执行任务
    • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
    • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。
image-20230112025138346

根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池

newFixedThreadPool

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

特点

  • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
    • 全部都是救急线程(60s 后可以回收)
    • 救急线程可以无限创建
  • 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
SynchronousQueue<Integer> integers = new SynchronousQueue<>();
new Thread(() -> {
try {
log.debug("putting {} ", 1);
integers.put(1);
log.debug("{} putted...", 1);

log.debug("putting...{} ", 2);
integers.put(2);
log.debug("{} putted...", 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();

sleep(1);

new Thread(() -> {
try {
log.debug("taking {}", 1);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();

sleep(1);

new Thread(() -> {
try {
log.debug("taking {}", 2);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t3").start();

输出结果

1
2
3
4
5
6
11:48:15.500 c.TestSynchronousQueue [t1] - putting 1
11:48:16.500 c.TestSynchronousQueue [t2] - taking 1
11:48:16.500 c.TestSynchronousQueue [t1] - 1 putted...
11:48:16.500 c.TestSynchronousQueue [t1] - putting...2
11:48:17.502 c.TestSynchronousQueue [t3] - taking 2
11:48:17.503 c.TestSynchronousQueue [t1] - 2 putted...

评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况。

newSingleThreadExecutor

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

使用场景:

希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放

区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

提交任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 执行任务
void execute(Runnable command);

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);

// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;

// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;

关闭线程池

shutdown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
线程池状态变为 SHUTDOWN
- 不会接收新任务
- 但已提交任务会执行完
- 此方法不会阻塞调用线程的执行
*/
void shutdown();


public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改线程池状态
advanceRunState(SHUTDOWN);
// 仅会打断空闲线程
interruptIdleWorkers();
onShutdown(); // 扩展点 ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)
tryTerminate();
}

shutdownNow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
线程池状态变为 STOP
- 不会接收新任务
- 会将队列中的任务返回
- 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();


public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改线程池状态
advanceRunState(STOP);
// 打断所有线程
interruptWorkers();
// 获取队列中剩余任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
// 尝试终结
tryTerminate();
return tasks;
}

其它方法

1
2
3
4
5
6
7
// 不在 RUNNING 状态的线程池,此方法就返回 true
boolean isShutdown();
// 线程池状态是否是 TERMINATED
boolean isTerminated();
// 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事
情,可以利用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

任务调度线程池

在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("task 1");

sleep(2);

}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
};
// 使用 timer 添加两个任务,希望它们都在 1s 后执行
// 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
}

输出

1
2
3
20:46:09.444 c.TestTimer [main] - start...
20:46:10.447 c.TestTimer [Timer-0] - task 1
20:46:12.448 c.TestTimer [Timer-0] - task 2

使用 ScheduledExecutorService 改写:

1
2
3
4
5
6
7
8
9
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,希望它们都在 1s 后执行
executor.schedule(() -> {
System.out.println("任务1,执行时间:" + new Date());
try { Thread.sleep(2000); } catch (InterruptedException e) { }
}, 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> {
System.out.println("任务2,执行时间:" + new Date());
}, 1000, TimeUnit.MILLISECONDS);

输出

1
2
任务1,执行时间:Thu Jan 03 12:45:17 CST 2019
任务2,执行时间:Thu Jan 03 12:45:17 CST 2019

scheduleAtFixedRate 例子:

1
2
3
4
5
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
log.debug("running...");
}, 1, 1, TimeUnit.SECONDS);

输出

1
2
3
4
5
21:45:43.167 c.TestTimer [main] - start...
21:45:44.215 c.TestTimer [pool-1-thread-1] - running...
21:45:45.215 c.TestTimer [pool-1-thread-1] - running...
21:45:46.215 c.TestTimer [pool-1-thread-1] - running...
21:45:47.215 c.TestTimer [pool-1-thread-1] - running...

scheduleAtFixedRate 例子(任务执行时间超过了间隔时间):

1
2
3
4
5
6
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
log.debug("running...");
sleep(2);
}, 1, 1, TimeUnit.SECONDS);

输出分析:一开始,延时 1s,接下来,由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s

1
2
3
4
5
21:44:30.311 c.TestTimer [main] - start...
21:44:31.360 c.TestTimer [pool-1-thread-1] - running...
21:44:33.361 c.TestTimer [pool-1-thread-1] - running...
21:44:35.362 c.TestTimer [pool-1-thread-1] - running...
21:44:37.362 c.TestTimer [pool-1-thread-1] - running...

scheduleWithFixedDelay 例子:

1
2
3
4
5
6
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleWithFixedDelay(()-> {
log.debug("running...");
sleep(2);
}, 1, 1, TimeUnit.SECONDS);

输出分析:一开始,延时 1s,scheduleWithFixedDelay 的间隔是 上一个任务结束 <-> 延时 <-> 下一个任务开始 所以间隔都是 3s

1
2
3
4
5
21:40:55.078 c.TestTimer [main] - start...
21:40:56.140 c.TestTimer [pool-1-thread-1] - running...
21:40:59.143 c.TestTimer [pool-1-thread-1] - running...
21:41:02.145 c.TestTimer [pool-1-thread-1] - running...
21:41:05.147 c.TestTimer [pool-1-thread-1] - running...

评价 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务

正确处理执行任务异常

方法1 主动捕捉异常

1
2
3
4
5
6
7
8
9
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> {
try {
log.debug("task1");
int i = 1 / 0;
} catch (Exception e) {
log.error("error:", e);
}
});

输出

1
2
3
4
5
6
7
8
9
21:59:04.558 c.TestTimer [pool-1-thread-1] - task1
21:59:04.562 c.TestTimer [pool-1-thread-1] - error:
java.lang.ArithmeticException: / by zero
at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

方法2 使用 Future

1
2
3
4
5
6
7
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<Boolean> f = pool.submit(() -> {
log.debug("task1");
int i = 1 / 0;
return true;
});
log.debug("result:{}", f.get());

输出

1
2
3
4
5
6
7
8
9
10
11
12
21:54:58.208 c.TestTimer [pool-1-thread-1] - task1
Exception in thread "main" java.util.concurrent.ExecutionException:
java.lang.ArithmeticException: / by zero
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at cn.itcast.n8.TestTimer.main(TestTimer.java:31)
Caused by: java.lang.ArithmeticException: / by zero
at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

Tomcat 线程池

Tomcat 中线程池的应用

  • LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
  • Acceptor 只负责【接收新的 socket 连接】
  • Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
  • 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
  • Executor 线程池中的工作线程最终负责【处理请求】

Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同

  • 如果总线程数达到 maximumPoolSize
    • 这时不会立刻抛 RejectedExecutionException 异常
    • 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常

源码 tomcat-7.0.42

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException rx) {
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException("Queue capacity is full.");
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
Thread.interrupted();
throw new RejectedExecutionException(x);
}
} else {
submittedCount.decrementAndGet();
throw rx;
}
}
}

TaskQueue.java

1
2
3
4
5
6
7
8
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
if ( parent.isShutdown() )
throw new RejectedExecutionException(
"Executor not running, can't force a command into the queue"
);
return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task
is rejected
}

Connector 配置

配置项 默认值 说明
acceptorThreadCount 1 acceptor 线程数量
pollerThreadCount 1 poller 线程数量
minSpareThreads 10 核心线程数,即 corePoolSize
maxThreads 200 最大线程数,即 maximumPoolSize
executor - Executor 名称,用来引用下面的 Executor

Executor 线程配置

配置项 默认值 说明
threadPriority 5 线程优先级
daemon true 是否守护线程
minSpareThreads 25 核心线程数,即 corePoolSize
maxThreads 200 最大线程数,即 maximumPoolSize
maxIdleTime 60000 线程生存时间,单位是毫秒,默认值即 1 分钟
maxQueueSize Integer.MAX_VALUE 队列长度
prestartminSpareThreads false 核心线程是否在服务器启动时启动
image-20230112031158412

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
@Slf4j(topic = "c.Test2")
public class Test2 {
// ThreadPoolExecutor 详解
// 线程池状态 TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
// 线程池构造方法

// 创建固定大小的线程池 newFixedThreadPool
public static void main(String[] args) {
// 创建只有两个线程的线程池 --两个核心线程
ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {

private AtomicInteger t = new AtomicInteger(1);

@Override
public Thread newThread(Runnable r) {
return new Thread(r, "myPool_h" + t.getAndIncrement());
}
});

// 核心线程并不会主动结束,即使任务都执行完毕,两个线程也依然在等待任务
pool.execute(() -> {
log.debug("1");
});

pool.execute(() -> {
log.debug("2");
});

pool.execute(() -> {
log.debug("3");
});

// 打印的线程名称依赖与一个线程工厂,线程工厂存在一个默认实现 DefaultThreadFactory,不需要我们自己实现
// 06:33:29.493 c.Test2 [pool-1-thread-2] - 2
// 06:33:29.493 c.Test2 [pool-1-thread-1] - 1
// 06:33:29.496 c.Test2 [pool-1-thread-2] - 3
// 自定义线程工厂
// 06:40:56.182 c.Test2 [myPool_h2] - 2
// 06:40:56.182 c.Test2 [myPool_h1] - 1
// 06:40:56.184 c.Test2 [myPool_h1] - 3
}
}


@Slf4j(topic = "c.Test3")
public class Test3 {
// newCachedThreadPool 缓冲线程池 特点为 所创建的线程均为救急线程,存活时间之后会自动回收,可以创建无限的救急线程
// 阻塞队列为 synchronousQueue 特点为 没有容量,
// 只有线程将队列中存在的任务取走之后才能继续存放任务,更像是用于线程间交换任务的队列
// synchronousQueue 主要为 newCachedThreadPool 配合使用
public static void main(String[] args) {
SynchronousQueue<Integer> integers = new SynchronousQueue<>();
new Thread(() -> {
try {
log.debug("putting {} ", 1);
integers.put(1);
log.debug("{} putted...", 1);

log.debug("putting...{} ", 2);
integers.put(2);
log.debug("{} putted...", 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();

sleep(1);

new Thread(() -> {
try {
log.debug("taking {}", 1);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();

sleep(1);

new Thread(() -> {
try {
log.debug("taking {}", 2);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t3").start();
}
}


@Slf4j(topic = "c.Test4")
public class Test4 {
// newSingleThreadExecutor --单例线程执行器
public static void main(String[] args) throws InterruptedException {
test2();
}

public static void test2() {
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.execute(() -> {
log.debug("1");
// 线程执行任务出现异常会自己结束
int i = 1 / 0;
});

pool.execute(() -> {
log.debug("2");
});

pool.execute(() -> {
log.debug("3");
});
}
}


@Slf4j(topic = "c.Test5")
public class Test5 {
// 线程池常见操作 API
/*
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果 --保护性暂停模式
<T> Future<T> submit(Callable<T> task);
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;

*/
public static void main(String[] args) throws ExecutionException, InterruptedException {
// test1();
// test2();
test3();
}

public static void test1() throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);

// submit 配合 callable 与 future 实现有返回结果的任务 future使用保护性暂停模式来接受另一个线程的结果
Future<String> future = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("running");
sleep(1);
return "success";
}
});

// 主线程阻塞等待 future 结果
log.debug("{}", future.get());
}

public static void test2() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<Object>> futures = pool.invokeAll(Arrays.asList(
() -> {
log.debug("begin-1");
sleep(1);
return "success -1";
},
() -> {
log.debug("begin-2");
sleep(2);
return "success -2";
},
() -> {
log.debug("begin-3");
sleep(1);
return "success -3";
}
));

// futures.stream().map(x -> x + " --2022").forEach(System.out::println);
futures.forEach(f -> {
try {
log.debug("{}", f.get());
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
});
}

public static void test3() throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newFixedThreadPool(2);
String result = pool.invokeAny(Arrays.asList(
() -> {
log.debug("begin-1");
sleep(2);
return "success -1";
},
() -> {
log.debug("begin-2");
sleep(1);
return "success -2";
},
() -> {
log.debug("begin-3");
sleep(3);
return "success -3";
}
));
log.debug("{}", result);
}
}


@Slf4j(topic = "c.Test6")
public class Test6 {
// shutdown 与 shutdownNow 关闭线程
public static void main(String[] args) throws InterruptedException {
// test1();
test2();
}

public static void test1() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);

Future<Integer> result1 = pool.submit(() -> {
log.debug("task 1 running...");
Thread.sleep(1000);
log.debug("task 1 finish...");
return 1;
});

Future<Integer> result2 = pool.submit(() -> {
log.debug("task 2 running...");
Thread.sleep(1000);
log.debug("task 2 finish...");
return 2;
});

Future<Integer> result3 = pool.submit(() -> {
log.debug("task 3 running...");
Thread.sleep(1000);
log.debug("task 3 finish...");
return 3;
});

// 不会取消正在执行的任务,也不会影响阻塞队列中的任务
// 都会将这些任务执行完毕,但执行了 shutdown 方法再提交任务则不能成功
log.debug("shutdown");
pool.shutdown();
// 等待线程池中的任务执行完成后再执行后续,或者等待指定时间
pool.awaitTermination(3, TimeUnit.SECONDS);
log.debug("other...");
}

public static void test2() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);

Future<Integer> result1 = pool.submit(() -> {
log.debug("task 1 running...");
Thread.sleep(1000);
log.debug("task 1 finish...");
return 1;
});

Future<Integer> result2 = pool.submit(() -> {
log.debug("task 2 running...");
Thread.sleep(1000);
log.debug("task 2 finish...");
return 2;
});

Future<Integer> result3 = pool.submit(() -> {
log.debug("task 3 running...");
Thread.sleep(1000);
log.debug("task 3 finish...");
return 3;
});

// 直接取消正在执行的任务,同时取消阻塞队列中的任务
log.debug("shutdown");
pool.shutdownNow();
log.debug("other...");
}
}


@Slf4j(topic = "c.Test8")
public class Test8 {
// java.util.Timer 实现定时任务 --缺点,所有任务都是由一个线程来调度,故而导致所有的任务都是串行执行,
// 同一时间只能存在一个任务在执行,一旦一个线程运行时间过长,或者出现阻塞与异常,都会影响到后续任务的执行
public static void main(String[] args) {
// test1();
// test2();
// test3();
test4();

}

// 任务间隔执行时间 --指任务执行完之后的间隔时间
public static void test4() {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
log.debug("start...");
pool.scheduleWithFixedDelay(() -> {
log.debug("running...");
sleep(2);
log.debug("over...");
}, 1, 1, TimeUnit.SECONDS);
}


// 任务调度线程池,定时执行任务,每秒执行一次
public static void test3() {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
log.debug("running...");
// 如果任务执行时间超过间隔时间,那么就会等到任务执行完后再进行下一轮任务,不会使任务重叠
// sleep(2);
}, 1, 1, TimeUnit.SECONDS);
}

// 任务调度线程池,改进 timer 操作
public static void test2() {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
// 如果使用只有一个核心线程的线程池,那么同样只能串行执行任务
// ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);

log.debug("start...");

pool.schedule(() -> {
log.debug("task1");
sleep(1);
// 即使一个线程出现异常,不影响其他线程工作,任务同样能被正常的执行
// int i = 1 / 0;
// 处理异常的方式
/*
1. try catch 自己主动处理异常
2. 使用 future 对象 future.get() 获取异常信息
*/
}, 1, TimeUnit.SECONDS);

pool.schedule(() -> {
log.debug("task2");
sleep(1);
}, 1, TimeUnit.SECONDS);
}

// Timer 实现多线程定时任务
public static void test1() {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("task 1");
sleep(1);
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 1");
}
};

log.debug("start...");
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
/*
执行结果
17:28:20.863 c.Test8 [main] - start...
17:28:21.865 c.Test8 [Timer-0] - task 1
17:28:22.881 c.Test8 [Timer-0] - task 1
*/
}
}


@Slf4j(topic = "c.Test9")
public class Test9 {
// 任务调度线程池应用
// 如何让每周四 18:00:00 定时执行任务?
public static void main(String[] args) {
// 获取当前时间 LocalDateTime jdk8新增操作时间的api 线程安全且更便于操作
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
// 获取周四时间
LocalDateTime time = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
// 如果 当前时间 > 本周周四,必须找到下周周四
if(now.compareTo(time) > 0) {
time = time.plusWeeks(1);
}
System.out.println(time);
// initialDelay 代表当前时间和周四的时间差
// period 一周的间隔时间
long initialDelay = Duration.between(now, time).toMillis();
long period = 1000 * 60 * 60 * 7 * 24;
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.scheduleAtFixedRate(() -> {
System.out.println("running...");
}, initialDelay, period, TimeUnit.MILLISECONDS);
}
}
Fork/Join

概念

Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算

所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解

Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率

Fork/Join 默认会创建与 cpu 核心数大小相同的线程池

使用

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Slf4j(topic = "c.AddTask")
class AddTask1 extends RecursiveTask<Integer> {
int n;
public AddTask1(int n) {
this.n = n;
}
@Override
public String toString() {
return "{" + n + '}';
}
@Override
protected Integer compute() {
// 如果 n 已经为 1,可以求得结果了
if (n == 1) {
log.debug("join() {}", n);
return n;
}

// 将任务进行拆分(fork)
AddTask1 t1 = new AddTask1(n - 1);
t1.fork();
log.debug("fork() {} + {}", n, t1);

// 合并(join)结果
int result = n + t1.join();
log.debug("join() {} + {} = {}", n, t1, result);
return result;
}
}

然后提交给 ForkJoinPool 来执行

1
2
3
4
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask1(5)));
}

结果

1
2
3
4
5
6
7
8
9
10
[ForkJoinPool-1-worker-0] - fork() 2 + {1}
[ForkJoinPool-1-worker-1] - fork() 5 + {4}
[ForkJoinPool-1-worker-0] - join() 1
[ForkJoinPool-1-worker-0] - join() 2 + {1} = 3
[ForkJoinPool-1-worker-2] - fork() 4 + {3}
[ForkJoinPool-1-worker-3] - fork() 3 + {2}
[ForkJoinPool-1-worker-3] - join() 3 + {2} = 6
[ForkJoinPool-1-worker-2] - join() 4 + {3} = 10
[ForkJoinPool-1-worker-1] - join() 5 + {4} = 15
15

图解分析

image-20230112122358697

改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class AddTask3 extends RecursiveTask<Integer> {

int begin;
int end;
public AddTask3(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
// 5, 5
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
// 4, 5
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}

// 1 5
int mid = (end + begin) / 2; // 3
AddTask3 t1 = new AddTask3(begin, mid); // 1,3
t1.fork();
AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5
t2.fork();
log.debug("fork() {} + {} = ?", t1, t2);
int result = t1.join() + t2.join();
log.debug("join() {} + {} = {}", t1, t2, result);
return result;
}
}

然后提交给 ForkJoinPool 来执行

1
2
3
4
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask3(1, 10)));
}

结果

1
2
3
4
5
6
7
8
[ForkJoinPool-1-worker-0] - join() 1 + 2 = 3
[ForkJoinPool-1-worker-3] - join() 4 + 5 = 9
[ForkJoinPool-1-worker-0] - join() 3
[ForkJoinPool-1-worker-1] - fork() {1,3} + {4,5} = ?
[ForkJoinPool-1-worker-2] - fork() {1,2} + {3,3} = ?
[ForkJoinPool-1-worker-2] - join() {1,2} + {3,3} = 6
[ForkJoinPool-1-worker-1] - join() {1,3} + {4,5} = 15
15

图解分析

image-20230112122534665

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
@Slf4j(topic = "c.Test10")
public class Test10 {
// fork/join 线程池的使用
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new MyTask(5)));
}
}

// 创建拆分任务
// RecursiveAction -无返回值 RecursiveTask -有返回值
class MyTask extends RecursiveTask<Integer> {

private int n;

public MyTask(int n) {
this.n = n;
}

@Override
protected Integer compute() {
// 终止条件
if (n == 1) {
return 1;
}
MyTask t1 = new MyTask(n - 1);
t1.fork(); // 让一个线程区执行此任务

int result = n + t1.join();
return result;
}
}


@Slf4j(topic = "c.Test11")
public class Test11 {
// fork/join 线程池详细执行细节
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new MyTask2(5)));

// new MyTask(5) 5+ new MyTask(4) 4 + new MyTask(3) 3 + new MyTask(2) 2 + new MyTask(1)
}
}

// 1~n 之间整数的和
@Slf4j(topic = "c.MyTask2")
class MyTask2 extends RecursiveTask<Integer> {

private int n;

public MyTask2(int n) {
this.n = n;
}

@Override
public String toString() {
return "{" + n + '}';
}

@Override
protected Integer compute() {
// 如果 n 已经为 1,可以求得结果了
if (n == 1) {
log.debug("join() {}", n);
return n;
}

// 将任务进行拆分(fork)
AddTask1 t1 = new AddTask1(n - 1);
t1.fork();
log.debug("fork() {} + {}", n, t1);

// 合并(join)结果
int result = n + t1.join();
log.debug("join() {} + {} = {}", n, t1, result);
return result;
}
}

@Slf4j(topic = "c.AddTask")
class AddTask1 extends RecursiveTask<Integer> {

int n;

public AddTask1(int n) {
this.n = n;
}

@Override
public String toString() {
return "{" + n + '}';
}

@Override
protected Integer compute() {
if (n == 1) {
log.debug("join() {}", n);
return n;
}
AddTask1 t1 = new AddTask1(n - 1);

t1.fork();
log.debug("fork() {} + {}", n, t1);
int result = n + t1.join();
log.debug("join() {} + {} = {}", n, t1, result);
return result;
}
}

@Slf4j(topic = "c.AddTask")
class AddTask2 extends RecursiveTask<Integer> {

int begin;
int end;

public AddTask2(int begin, int end) {
this.begin = begin;
this.end = end;
}

@Override
public String toString() {
return "{" + begin + "," + end + '}';
}

@Override
protected Integer compute() {
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;

AddTask2 t1 = new AddTask2(begin, mid - 1);
t1.fork();
AddTask2 t2 = new AddTask2(mid + 1, end);
t2.fork();
log.debug("fork() {} + {} + {} = ?", mid, t1, t2);

int result = mid + t1.join() + t2.join();
log.debug("join() {} + {} + {} = {}", mid, t1, t2, result);
return result;
}
}

@Slf4j(topic = "c.AddTask")
class AddTask3 extends RecursiveTask<Integer> {

// 队伍拆分优化
int begin;
int end;

public AddTask3(int begin, int end) {
this.begin = begin;
this.end = end;
}

@Override
public String toString() {
return "{" + begin + "," + end + '}';
}

@Override
protected Integer compute() {
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;

AddTask3 t1 = new AddTask3(begin, mid);
t1.fork();
AddTask3 t2 = new AddTask3(mid + 1, end);
t2.fork();
log.debug("fork() {} + {} = ?", t1, t2);

int result = t1.join() + t2.join();
log.debug("join() {} + {} = {}", t1, t2, result);
return result;
}
}

J.U.C 并发包工具

AQS

概述

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架

AQS原理

ReentrantLock原理篇

ReentrantLock原理篇

读写锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Slf4j(topic = "c.Test13")
public class Test13 {
// 可重入读写锁
// 读-读 可并发 不互斥
// 读-写 互斥
// 写-写 互斥
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();

new Thread(() -> {
dataContainer.read();
}, "t2").start();
}
}

@Slf4j(topic = "c.DataContainer")
class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

public Object read() {
log.debug("获取读锁...");
r.lock();
try {
log.debug("读取");
sleep(1);
return data;
} finally {
log.debug("释放读锁...");
r.unlock();
}
}

public void write() {
log.debug("获取写锁...");
w.lock();
try {
log.debug("写入");
sleep(1);
} finally {
log.debug("释放写锁...");
w.unlock();
}
}
}
ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。 类似于数据库中的 select … from … lock in share mode

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
log.debug("获取读锁...");
r.lock();
try {
log.debug("读取");
sleep(1);
return data;
} finally {
log.debug("释放读锁...");
r.unlock();
}
}
public void write() {
log.debug("获取写锁...");
w.lock();
try {
log.debug("写入");
sleep(1);
} finally {
log.debug("释放写锁...");
w.unlock();
}
}
}

测试 读锁-读锁 可以并发

1
2
3
4
5
6
7
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
new Thread(() -> {
dataContainer.read();
}, "t2").start();

输出结果,从这里可以看到 Thread-0 锁定期间,Thread-1 的读操作不受影响

1
2
3
4
5
6
14:05:14.341 c.DataContainer [t2] - 获取读锁...
14:05:14.341 c.DataContainer [t1] - 获取读锁...
14:05:14.345 c.DataContainer [t1] - 读取
14:05:14.345 c.DataContainer [t2] - 读取
14:05:15.365 c.DataContainer [t2] - 释放读锁...
14:05:15.386 c.DataContainer [t1] - 释放读锁...

测试 读锁-写锁 相互阻塞

1
2
3
4
5
6
7
8
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
Thread.sleep(100);
new Thread(() -> {
dataContainer.write();
}, "t2").start();

输出结果

1
2
3
4
5
6
14:04:21.838 c.DataContainer [t1] - 获取读锁...
14:04:21.838 c.DataContainer [t2] - 获取写锁...
14:04:21.841 c.DataContainer [t2] - 写入
14:04:22.843 c.DataContainer [t2] - 释放写锁...
14:04:22.843 c.DataContainer [t1] - 读取
14:04:23.843 c.DataContainer [t1] - 释放读锁...

写锁-写锁 也是相互阻塞的,这里就不测试了

注意

  • 读锁不支持条件变量
  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
1
2
3
4
5
6
7
8
9
10
11
12
r.lock();
try {
// ...
w.lock();
try {
// ...
} finally{
w.unlock();
}
} finally{
r.unlock();
}
  • 重入时降级支持:即持有写锁的情况下去获取读锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class CachedData {
Object data;
// 是否有效,如果失效,需要重新计算 data
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 获取写锁前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock();
}
}
// 自己用完数据, 释放读锁
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}

应用–缓存

读写锁原理

StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

加解读锁

1
2
long stamp = lock.readLock();
lock.unlockRead(stamp);

加解写锁

1
2
long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

1
2
3
4
5
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();
public DataContainerStamped(int data) {
this.data = data;
}
public int read(int readTime) {
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking...{}", stamp);
sleep(readTime);
if (lock.validate(stamp)) {
log.debug("read finish...{}, data:{}", stamp, data);
return data;
}
// 锁升级 - 读锁
log.debug("updating to read lock... {}", stamp);
try {
stamp = lock.readLock();
log.debug("read lock {}", stamp);
sleep(readTime);
log.debug("read finish...{}, data:{}", stamp, data);
return data;
} finally {
log.debug("read unlock {}", stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = lock.writeLock();
log.debug("write lock {}", stamp);
try {
sleep(2);
this.data = newData;
} finally {
log.debug("write unlock {}", stamp);
lock.unlockWrite(stamp);
}
}
}

测试 读-读 可以优化

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.read(0);
}, "t2").start();
}

输出结果,可以看到实际没有加读锁

1
2
3
4
15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1
15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1

测试 读-写 时优化读补加读锁

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.write(100);
}, "t2").start();
}

输出结果

1
2
3
4
5
6
7
15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256
15:57:00.717 c.DataContainerStamped [t2] - write lock 384
15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256
15:57:02.719 c.DataContainerStamped [t2] - write unlock 384
15:57:02.719 c.DataContainerStamped [t1] - read lock 513
15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000
15:57:03.719 c.DataContainerStamped [t1] - read unlock 513

注意

  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入
Semaphore

Semaphore[ˈsɛməˌfɔr] 信号量,用来限制能同时访问共享资源的线程上限。–一种特殊的读写锁,性能更高

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 3. 获取许可
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(1);
log.debug("end...");
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
07:35:15.485 c.TestSemaphore [Thread-2] - running...
07:35:15.485 c.TestSemaphore [Thread-1] - running...
07:35:15.485 c.TestSemaphore [Thread-0] - running...
07:35:16.490 c.TestSemaphore [Thread-2] - end...
07:35:16.490 c.TestSemaphore [Thread-0] - end...
07:35:16.490 c.TestSemaphore [Thread-1] - end...
07:35:16.490 c.TestSemaphore [Thread-3] - running...
07:35:16.490 c.TestSemaphore [Thread-5] - running...
07:35:16.490 c.TestSemaphore [Thread-4] - running...
07:35:17.490 c.TestSemaphore [Thread-5] - end...
07:35:17.490 c.TestSemaphore [Thread-4] - end...
07:35:17.490 c.TestSemaphore [Thread-3] - end...
07:35:17.490 c.TestSemaphore [Thread-6] - running...
07:35:17.490 c.TestSemaphore [Thread-7] - running...
07:35:17.490 c.TestSemaphore [Thread-9] - running...
07:35:18.491 c.TestSemaphore [Thread-6] - end...
07:35:18.491 c.TestSemaphore [Thread-7] - end...
07:35:18.491 c.TestSemaphore [Thread-9] - end...
07:35:18.491 c.TestSemaphore [Thread-8] - running...
07:35:19.492 c.TestSemaphore [Thread-8] - end...

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@Slf4j(topic = "c.Test14")
public class Test14 {
// StampedLock简单应用案例
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.read(0);
}, "t2").start();
}
}

@Slf4j(topic = "c.DataContainerStamped")
class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();

public DataContainerStamped(int data) {
this.data = data;
}

public int read(int readTime) {
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking...{}", stamp);
sleep(readTime);
if (lock.validate(stamp)) {
log.debug("read finish...{}, data:{}", stamp, data);
return data;
}
// 锁升级 - 从乐观读锁升级到读锁
log.debug("updating to read lock... {}", stamp);
try {
stamp = lock.readLock();
log.debug("read lock {}", stamp);
sleep(readTime);
log.debug("read finish...{}, data:{}", stamp, data);
return data;
} finally {
log.debug("read unlock {}", stamp);
lock.unlockRead(stamp);
}
}

public void write(int newData) {
long stamp = lock.writeLock();
log.debug("write lock {}", stamp);
try {
sleep(2);
this.data = newData;
} finally {
log.debug("write unlock {}", stamp);
lock.unlockWrite(stamp);
}
}
}


@Slf4j(topic = "c.Test15")
public class Test15 {
// semaphore简单应用案例
public static void main(String[] args) {
// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3);

// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(1);
log.debug("end...");
} finally {
semaphore.release();
}
}).start();
}
}
}

Semaphore应用

应用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
package JUC.chapter_8_JUCTools;

import lombok.extern.slf4j.Slf4j;

import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicIntegerArray;

@Slf4j(topic = "c.Test16")
public class Test16 {
// Semaphore应用案例 --单机限制访问共享资源的线程数(简单限流)
// 资源数与线程数相同时,使用Semaphore限流就比较合适 --类似案例有数据库连接池
// Semaphore优化数据库连接池
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection conn = pool.borrow();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(conn);
}).start();
}
}
}

@Slf4j(topic = "c.Pool")
class Pool {
// 1. 连接池大小
private final int poolSize;

// 2. 连接对象数组
private Connection[] connections;

// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;

private Semaphore semaphore;

// 4. 构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
// 让许可数与资源数一致
this.semaphore = new Semaphore(poolSize);
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i+1));
}
}

// 5. 借连接
public Connection borrow() {// t1, t2, t3
// 获取许可
try {
semaphore.acquire(); // 没有许可的线程,在此等待
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if(states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 不会执行到这里
return null;
}
// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
log.debug("free {}", conn);
semaphore.release();
break;
}
}
}
}

class MockConnection implements Connection {

private String name;

public MockConnection(String name) {
this.name = name;
}

@Override
public String toString() {
return "MockConnection{" +
"name='" + name + '\'' +
'}';
}

@Override
public Statement createStatement() throws SQLException {
return null;
}

@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return null;
}

@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return null;
}

@Override
public String nativeSQL(String sql) throws SQLException {
return null;
}

@Override
public void setAutoCommit(boolean autoCommit) throws SQLException {

}

@Override
public boolean getAutoCommit() throws SQLException {
return false;
}

@Override
public void commit() throws SQLException {

}

@Override
public void rollback() throws SQLException {

}

@Override
public void close() throws SQLException {

}

@Override
public boolean isClosed() throws SQLException {
return false;
}

@Override
public DatabaseMetaData getMetaData() throws SQLException {
return null;
}

@Override
public void setReadOnly(boolean readOnly) throws SQLException {

}

@Override
public boolean isReadOnly() throws SQLException {
return false;
}

@Override
public void setCatalog(String catalog) throws SQLException {

}

@Override
public String getCatalog() throws SQLException {
return null;
}

@Override
public void setTransactionIsolation(int level) throws SQLException {

}

@Override
public int getTransactionIsolation() throws SQLException {
return 0;
}

@Override
public SQLWarning getWarnings() throws SQLException {
return null;
}

@Override
public void clearWarnings() throws SQLException {

}

@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}

@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}

@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
return null;
}

@Override
public Map<String, Class<?>> getTypeMap() throws SQLException {
return null;
}

@Override
public void setTypeMap(Map<String, Class<?>> map) throws SQLException {

}

@Override
public void setHoldability(int holdability) throws SQLException {

}

@Override
public int getHoldability() throws SQLException {
return 0;
}

@Override
public Savepoint setSavepoint() throws SQLException {
return null;
}

@Override
public Savepoint setSavepoint(String name) throws SQLException {
return null;
}

@Override
public void rollback(Savepoint savepoint) throws SQLException {

}

@Override
public void releaseSavepoint(Savepoint savepoint) throws SQLException {

}

@Override
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}

@Override
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}

@Override
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return null;
}

@Override
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
return null;
}

@Override
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
return null;
}

@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
return null;
}

@Override
public Clob createClob() throws SQLException {
return null;
}

@Override
public Blob createBlob() throws SQLException {
return null;
}

@Override
public NClob createNClob() throws SQLException {
return null;
}

@Override
public SQLXML createSQLXML() throws SQLException {
return null;
}

@Override
public boolean isValid(int timeout) throws SQLException {
return false;
}

@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {

}

@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {

}

@Override
public String getClientInfo(String name) throws SQLException {
return null;
}

@Override
public Properties getClientInfo() throws SQLException {
return null;
}

@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
return null;
}

@Override
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
return null;
}

@Override
public void setSchema(String schema) throws SQLException {

}

@Override
public String getSchema() throws SQLException {
return null;
}

@Override
public void abort(Executor executor) throws SQLException {

}

@Override
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {

}

@Override
public int getNetworkTimeout() throws SQLException {
return 0;
}

@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}

@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}

Semaphore原理

CountdownLatch
理论与基本应用

用来进行线程同步协作,等待所有线程完成倒计时。

其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
log.debug("begin...");
sleep(1);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
new Thread(() -> {
log.debug("begin...");
sleep(2);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
new Thread(() -> {
log.debug("begin...");
sleep(1.5);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
log.debug("waiting...");
latch.await();
log.debug("wait end...");
}

输出

1
2
3
4
5
6
7
8
18:44:00.778 c.TestCountDownLatch [main] - waiting...
18:44:00.778 c.TestCountDownLatch [Thread-2] - begin...
18:44:00.778 c.TestCountDownLatch [Thread-0] - begin...
18:44:00.778 c.TestCountDownLatch [Thread-1] - begin...
18:44:01.782 c.TestCountDownLatch [Thread-0] - end...2
18:44:02.283 c.TestCountDownLatch [Thread-2] - end...1
18:44:02.782 c.TestCountDownLatch [Thread-1] - end...0
18:44:02.782 c.TestCountDownLatch [main] - wait end...

可以配合线程池使用,改进如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
ExecutorService service = Executors.newFixedThreadPool(4);
service.submit(() -> {
log.debug("begin...");
sleep(1);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
log.debug("begin...");
sleep(1.5);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
log.debug("begin...");
sleep(2);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(()->{
try {
log.debug("waiting...");
latch.await();
log.debug("wait end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

输出

1
2
3
4
5
6
7
8
18:52:25.831 c.TestCountDownLatch [pool-1-thread-3] - begin...
18:52:25.831 c.TestCountDownLatch [pool-1-thread-1] - begin...
18:52:25.831 c.TestCountDownLatch [pool-1-thread-2] - begin...
18:52:25.831 c.TestCountDownLatch [pool-1-thread-4] - waiting...
18:52:26.835 c.TestCountDownLatch [pool-1-thread-1] - end...2
18:52:27.335 c.TestCountDownLatch [pool-1-thread-2] - end...1
18:52:27.835 c.TestCountDownLatch [pool-1-thread-3] - end...0
18:52:27.835 c.TestCountDownLatch [pool-1-thread-4] - wait end...
应用案例-同步等待多线程准备完毕(类wzry加载页面)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
AtomicInteger num = new AtomicInteger(0);
ExecutorService service = Executors.newFixedThreadPool(10, (r) -> {
return new Thread(r, "t" + num.getAndIncrement());
});
CountDownLatch latch = new CountDownLatch(10);
String[] all = new String[10];
Random r = new Random();
for (int j = 0; j < 10; j++) {
int x = j;
service.submit(() -> {
for (int i = 0; i <= 100; i++) {
try {
Thread.sleep(r.nextInt(100));
} catch (InterruptedException e) {
}
all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")";
System.out.print("\r" + Arrays.toString(all));
}
latch.countDown();
});
}
latch.await();
System.out.println("\n游戏开始...");
service.shutdown();

输出

1
[t0(52%), t1(47%), t2(51%), t3(40%), t4(49%), t5(44%), t6(49%), t7(52%), t8(46%), t9(46%)]

结果

1
2
[t0(100%), t1(100%), t2(100%), t3(100%), t4(100%), t5(100%), t6(100%), t7(100%), t8(100%),t9(100%)]
游戏开始...
应用案例-同步等待多个远程调用结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RestController
public class TestCountDownlatchController {
@GetMapping("/order/{id}")
public Map<String, Object> order(@PathVariable int id) {
HashMap<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("total", "2300.00");
sleep(2000);
return map;
}
@GetMapping("/product/{id}")
public Map<String, Object> product(@PathVariable int id) {
HashMap<String, Object> map = new HashMap<>();
if (id == 1) {
map.put("name", "小爱音箱");
map.put("price", 300);
} else if (id == 2) {
map.put("name", "小米手机");
map.put("price", 2000);
}
map.put("id", id);
sleep(1000);
return map;
}
@GetMapping("/logistics/{id}")
public Map<String, Object> logistics(@PathVariable int id) {
HashMap<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", "中通快递");
sleep(2500);
return map;
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

rest 远程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
RestTemplate restTemplate = new RestTemplate();
log.debug("begin");
ExecutorService service = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(4);
Future<Map<String,Object>> f1 = service.submit(() -> {
Map<String, Object> r =
restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
return r;
});
Future<Map<String, Object>> f2 = service.submit(() -> {
Map<String, Object> r =
restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);
return r;
});
Future<Map<String, Object>> f3 = service.submit(() -> {
Map<String, Object> r =
restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
return r;
});
Future<Map<String, Object>> f4 = service.submit(() -> {
Map<String, Object> r =
restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
return r;
});
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
log.debug("执行完毕");
service.shutdown();

执行结果

1
2
3
4
5
6
19:51:39.711 c.TestCountDownLatch [main] - begin
{total=2300.00, id=1}
{price=300, name=小爱音箱, id=1}
{price=2000, name=小米手机, id=2}
{name=中通快递, id=1}
19:51:42.407 c.TestCountDownLatch [main] - 执行完毕

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
@Slf4j(topic = "c.Test17")
public class Test17 {
// CountDownLatch --线程倒数计数器
public static void main(String[] args) throws InterruptedException {
// test1();
// test2();
test3();
}

// 高级应用 --等待多个远程请求结束后汇总

// 模拟玩家加载进度,当所有玩家进度都达到100%时,主线程才能继续
private static void test3() throws InterruptedException {
AtomicInteger num = new AtomicInteger(0);
ExecutorService service = Executors.newFixedThreadPool(10, (r) -> {
return new Thread(r, "t" + num.getAndIncrement());
});
CountDownLatch latch = new CountDownLatch(10);
String[] all = new String[10];
Random r = new Random();
for (int j = 0; j < 10; j++) {
int x = j;
service.submit(() -> {
for (int i = 0; i <= 100; i++) {
try {
Thread.sleep(r.nextInt(100));
} catch (InterruptedException e) {
}
all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")";
// \r 回退符号打印覆盖
System.out.print("\r" + Arrays.toString(all));
}
latch.countDown();
});
}
latch.await();
System.out.println("\n游戏开始...");
service.shutdown();
}


// CountDownLatch配合线程池的应用案例
private static void test2() {
CountDownLatch latch = new CountDownLatch(3);
ExecutorService service = Executors.newFixedThreadPool(4);
service.submit(() -> {
log.debug("begin...");
sleep(1);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
log.debug("begin...");
sleep(1.5);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
log.debug("begin...");
sleep(2);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(()->{
try {
log.debug("waiting...");
latch.await();
log.debug("wait end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

// 简单案例
private static void test1() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);

new Thread(() -> {
log.debug("begin...");
sleep(1);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();

new Thread(() -> {
log.debug("begin...");
sleep(2);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();

new Thread(() -> {
log.debug("begin...");
sleep(1.5);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();

log.debug("waiting...");
// 等待线程倒数计数
latch.await();
log.debug("wait end...");
}
}
CyclicBarrier

CyclicBarrier[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执 行到某个需要”同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
new Thread(()->{
System.out.println("线程1开始.."+new Date());
try {
cb.await(); // 当个数不足时,等待
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("线程1继续向下运行..."+new Date());
}).start();
new Thread(()->{
System.out.println("线程2开始.."+new Date());
try { Thread.sleep(2000); } catch (InterruptedException e) { }
try {
cb.await(); // 2 秒后,线程个数够2,继续运行
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("线程2继续向下运行..."+new Date());
}).start();

注意

CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』

线程安全集合类
image-20230113135129237

线程安全集合类可以分为三大类:

  • 遗留的线程安全集合如 HashtableVector
  • 使用 Collections 装饰的线程安全集合,如:
    • Collections.synchronizedCollection
    • Collections.synchronizedList
    • Collections.synchronizedMap
    • Collections.synchronizedSet
    • Collections.synchronizedNavigableMap
    • Collections.synchronizedNavigableSet
    • Collections.synchronizedSortedMap
    • Collections.synchronizedSortedSet
  • java.util.concurrent.*

重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词: BlockingCopyOnWriteConcurrent

  • Blocking 大部分实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器
    • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
    • 弱一致性
      • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
      • 求大小弱一致性,size 操作未必是 100% 准确
      • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出 ConcurrentModificationException,不再继续遍历

ConcurrentHashMap

练习:单词计数

生成测试数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static final String ALPHA = "abcedfghijklmnopqrstuvwxyz";
public static void main(String[] args) {
int length = ALPHA.length();
int count = 200;
List<String> list = new ArrayList<>(length * count);
for (int i = 0; i < length; i++) {
char ch = ALPHA.charAt(i);
for (int j = 0; j < count; j++) {
list.add(String.valueOf(ch));
}
}
Collections.shuffle(list);
for (int i = 0; i < 26; i++) {
try (PrintWriter out = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream("tmp/" + (i+1) + ".txt")))) {
String collect = list.subList(i * count, (i + 1) * count).stream()
.collect(Collectors.joining("\n"));
out.print(collect);
} catch (IOException e) {
}
}
}

模版代码,模版代码中封装了多线程读取文件的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static <V> void demo(Supplier<Map<String,V>> supplier,
BiConsumer<Map<String,V>,List<String>> consumer) {
Map<String, V> counterMap = supplier.get();
List<Thread> ts = new ArrayList<>();
for (int i = 1; i <= 26; i++) {
int idx = i;
Thread thread = new Thread(() -> {
List<String> words = readFromFile(idx);
consumer.accept(counterMap, words);
});
ts.add(thread);
}
ts.forEach(t->t.start());
ts.forEach(t-> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(counterMap);
}
public static List<String> readFromFile(int i) {
ArrayList<String> words = new ArrayList<>();
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/"
+ i +".txt")))) {
while(true) {
String word = in.readLine();
if(word == null) {
break;
}
words.add(word);
}
return words;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

你要做的是实现两个参数

  • 一是提供一个 map 集合,用来存放每个单词的计数结果,key 为单词,value 为计数
  • 二是提供一组操作,保证计数的安全性,会传递 map 集合以及 单词 List

正确结果输出应该是每个单词出现 200 次

1
{a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200}

下面的实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
demo(
// 创建 map 集合
// 创建 ConcurrentHashMap 对不对?
() -> new HashMap<String, Integer>(),
// 进行计数
(map, words) -> {
for (String word : words) {
Integer counter = map.get(word);
int newValue = counter == null ? 1 : counter + 1;
map.put(word, newValue);
}
}
);

有没有问题?请改进

参考解答1

1
2
3
4
5
6
7
8
9
demo(
() -> new ConcurrentHashMap<String, LongAdder>(),
(map, words) -> {
for (String word : words) {
// 注意不能使用 putIfAbsent,此方法返回的是上一次的 value,首次调用返回 null
map.computeIfAbsent(word, (key) -> new LongAdder()).increment();
}
}
);

参考解答2

1
2
3
4
5
6
7
8
9
demo(
() -> new ConcurrentHashMap<String, Integer>(),
(map, words) -> {
for (String word : words) {
// 函数式编程,无需原子变量
map.merge(word, 1, Integer::sum);
}
}
);

ConcurrentHashMap 原理

BlockingQueue

原理分析

  • LinkedBlockingQueue 入队出队
  • LinkedBlockingQueue 安全分析
  • LinkedBlockingQueue put
  • LinkedBlockingQueue 性能分析 对比array

LinkedBlockingQueue 原理

ConcurrentLinkedQueue

ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是

  • 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
  • dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
  • 只是这【锁】使用了 cas 来实现

事实上,ConcurrentLinkedQueue 应用还是非常广泛的

例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了 ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用

ConcurrentLinkedQueue原理

CopyOnWriteArrayList

CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更 改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean add(E e) {
synchronized (lock) {
// 获取旧的数组
Object[] es = getArray();
int len = es.length;
// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
es = Arrays.copyOf(es, len + 1);
// 添加新元素
es[len] = e;
// 替换旧的数组
setArray(es);
return true;
}
}

这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized

其它读操作并未加锁,例如:

1
2
3
4
5
6
7
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (Object x : getArray()) {
@SuppressWarnings("unchecked") E e = (E) x;
action.accept(e);
}
}

适合『读多写少』的应用场景

Get 弱一致性

image-20230113145118933
时间点 操作
1 Thread-0 getArray()
2 Thread-1 getArray()
3 Thread-1 setArray(arrayCopy)
5 Thread-0 array[index]

不容易测试,但问题确实存在

迭代器弱一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator();
new Thread(() -> {
list.remove(0);
System.out.println(list);
}).start();
sleep1s();
while (iter.hasNext()) {
System.out.println(iter.next());
}

不要觉得弱一致性就不好

  • 数据库的 MVCC 都是弱一致性的表现
  • 并发高和一致性是矛盾的,需要权衡

应用

并发提高CPU效率

多线程跑满CPU

测试环境
  • 基准测试工具选择,使用了比较靠谱的 JMH,它会执行程序预热,执行多次测试并平均
  • cpu 核数限制,有两种思路
    1. 使用虚拟机,分配合适的核
    2. 使用 msconfig,分配合适的核,需要重启比较麻烦
  • 并行计算方式的选择
    1. 最初想直接使用 parallel stream,后来发现它有自己的问题
    2. 改为了自己手动控制 thread,实现简单的并行计算
测试代码

环境

1
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=org.sample -DartifactId=test -Dversion=1.0

代码

jmh_performance工程

充分利用多核CPU效率 –1000_000_00数之和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
public class MyBenchmark {
static int[] ARRAY = new int[1000_000_00];
static {
Arrays.fill(ARRAY, 1);
}
@Benchmark
public int c() throws Exception {
int[] array = ARRAY;
FutureTask<Integer> t1 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 250_000_00;i++) {
sum += array[0+i];
}
return sum;
});
FutureTask<Integer> t2 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 250_000_00;i++) {
sum += array[250_000_00+i];
}
return sum;
});
FutureTask<Integer> t3 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 250_000_00;i++) {
sum += array[500_000_00+i];
}
return sum;
});
FutureTask<Integer> t4 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 250_000_00;i++) {
sum += array[750_000_00+i];
}
return sum;
});
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
new Thread(t4).start();
return t1.get() + t2.get() + t3.get()+ t4.get();
}
@Benchmark
public int d() throws Exception {
int[] array = ARRAY;
FutureTask<Integer> t1 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 1000_000_00;i++) {
sum += array[0+i];
}
return sum;
});
new Thread(t1).start();
return t1.get();
}
}

使用benchmarks.jar jar包测试

1
2
# 设置最大使用内存
java -jar -Xmx2G benchmarks.jar
结果案例

双核 CPU(4个逻辑CPU)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
C:\Users\lenovo\eclipse-workspace\test>java -jar target/benchmarks.jar
# VM invoker: C:\Program Files\Java\jdk-11\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.MyBenchmark.c
# Run progress: 0.00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration 1: 0.022 s/op
# Warmup Iteration 2: 0.019 s/op
# Warmup Iteration 3: 0.020 s/op
Iteration 1: 0.020 s/op
Iteration 2: 0.020 s/op
Iteration 3: 0.020 s/op
Iteration 4: 0.020 s/op
Iteration 5: 0.020 s/op
Result: 0.020 ±(99.9%) 0.001 s/op [Average]
Statistics: (min, avg, max) = (0.020, 0.020, 0.020), stdev = 0.000
Confidence interval (99.9%): [0.019, 0.021]
# VM invoker: C:\Program Files\Java\jdk-11\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.MyBenchmark.d
# Run progress: 50.00% complete, ETA 00:00:10
# Fork: 1 of 1
# Warmup Iteration 1: 0.042 s/op
# Warmup Iteration 2: 0.042 s/op
# Warmup Iteration 3: 0.041 s/op
Iteration 1: 0.043 s/op
Iteration 2: 0.042 s/op
Iteration 3: 0.042 s/op
Iteration 4: 0.044 s/op
Iteration 5: 0.042 s/op
Result: 0.043 ±(99.9%) 0.003 s/op [Average]
Statistics: (min, avg, max) = (0.042, 0.043, 0.044), stdev = 0.001
Confidence interval (99.9%): [0.040, 0.045]
# Run complete. Total time: 00:00:20

Benchmark Mode Samples Score Score error Units
o.s.MyBenchmark.c avgt 5 0.020 0.001 s/op
o.s.MyBenchmark.d avgt 5 0.043 0.003 s/op

单核 CPU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
C:\Users\lenovo\eclipse-workspace\test>java -jar target/benchmarks.jar
# VM invoker: C:\Program Files\Java\jdk-11\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.MyBenchmark.c
# Run progress: 0.00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration 1: 0.064 s/op
# Warmup Iteration 2: 0.052 s/op
# Warmup Iteration 3: 1.127 s/op
Iteration 1: 0.053 s/op
Iteration 2: 0.052 s/op
Iteration 3: 0.053 s/op
Iteration 4: 0.057 s/op
Iteration 5: 0.088 s/op
Result: 0.061 ±(99.9%) 0.060 s/op [Average]
Statistics: (min, avg, max) = (0.052, 0.061, 0.088), stdev = 0.016
Confidence interval (99.9%): [0.001, 0.121]
# VM invoker: C:\Program Files\Java\jdk-11\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.MyBenchmark.d
# Run progress: 50.00% complete, ETA 00:00:11
# Fork: 1 of 1
# Warmup Iteration 1: 0.054 s/op
# Warmup Iteration 2: 0.053 s/op
# Warmup Iteration 3: 0.051 s/op
Iteration 1: 0.096 s/op
Iteration 2: 0.054 s/op
Iteration 3: 0.065 s/op
Iteration 4: 0.050 s/op
Iteration 5: 0.055 s/op
Result: 0.064 ±(99.9%) 0.071 s/op [Average]
Statistics: (min, avg, max) = (0.050, 0.064, 0.096), stdev = 0.018
Confidence interval (99.9%): [-0.007, 0.135]
# Run complete. Total time: 00:00:22



Benchmark Mode Samples Score Score error Units
o.s.MyBenchmark.c avgt 5 0.061 0.060 s/op
o.s.MyBenchmark.d avgt 5 0.064 0.071 s/op

自测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
C:\Users\黎明\Desktop>java -jar -Xmx2G benchmarks.jar
# VM invoker: D:\JAVA\jre1.8_0_211\bin\java.exe
# VM options: -Xmx2G
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.itcast.MyBenchmark.a

# Run progress: 0.00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration 1: 1.299 ns/op
# Warmup Iteration 2: 1.318 ns/op
# Warmup Iteration 3: 1.341 ns/op
Iteration 1: 1.356 ns/op
Iteration 2: 1.312 ns/op
Iteration 3: 1.297 ns/op
Iteration 4: 1.301 ns/op
Iteration 5: 1.349 ns/op


Result: 1.323 ±(99.9%) 0.105 ns/op [Average]
Statistics: (min, avg, max) = (1.297, 1.323, 1.356), stdev = 0.027
Confidence interval (99.9%): [1.218, 1.428]


# VM invoker: D:\JAVA\jre1.8_0_211\bin\java.exe
# VM options: -Xmx2G
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.itcast.MyBenchmark.b

# Run progress: 50.00% complete, ETA 00:00:10
# Fork: 1 of 1
# Warmup Iteration 1: 1.324 ns/op
# Warmup Iteration 2: 1.351 ns/op
# Warmup Iteration 3: 1.369 ns/op
Iteration 1: 1.379 ns/op
Iteration 2: 1.366 ns/op
Iteration 3: 1.357 ns/op
Iteration 4: 1.313 ns/op
Iteration 5: 1.374 ns/op


Result: 1.358 ±(99.9%) 0.101 ns/op [Average]
Statistics: (min, avg, max) = (1.313, 1.358, 1.379), stdev = 0.026
Confidence interval (99.9%): [1.257, 1.459]


# Run complete. Total time: 00:00:20

Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.323 0.105 ns/op
c.i.MyBenchmark.b avgt 5 1.358 0.101 ns/op

线程资源合理限制

限制CPU资源持续占用

三种实现方式

  1. sleep实现
  2. wait实现
  3. 条件变量实现
sleep实现

在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序

1
2
3
4
5
6
7
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

测试结果

image-20230108192126036 image-20230108192155651

加上sleep之后

image-20230108192301109
  • 可以用 wait 或 条件变量达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
  • sleep 适用于无需锁同步的场景
wait实现
1
2
3
4
5
6
7
8
9
10
synchronized(锁对象) {
while(条件不满足) {
try {
锁对象.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
}
条件变量实现
1
2
3
4
5
6
7
8
9
10
11
12
13
lock.lock();
try {
while(条件不满足) {
try {
条件变量.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
} finally {
lock.unlock();
}

限制共享资源使用

semaphore 实现

  • 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
  • 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Slf4j(topic = "c.Pool")
class Pool {
// 1. 连接池大小
private final int poolSize;
// 2. 连接对象数组
private Connection[] connections;
// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;
private Semaphore semaphore;// 4. 构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
// 让许可数与资源数一致
this.semaphore = new Semaphore(poolSize);
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i+1));
}
}
// 5. 借连接
public Connection borrow() {// t1, t2, t3
// 获取许可
try {
semaphore.acquire(); // 没有许可的线程,在此等待
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if(states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 不会执行到这里
return null;
}
// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
log.debug("free {}", conn);
semaphore.release();
break;
}
}
}
}

单位时间内限流实现

guava 实现

1
2
3
4
5
6
7
8
9
@RestController
public class TestController {
private RateLimiter limiter = RateLimiter.create(50);
@GetMapping("/test")
public String test() {
// limiter.acquire();
return "ok";
}
}

没有限流之前

1
ab -c 10 -t 10 http://localhost:8080/test

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Finished 24706 requests
Server Software:
Server Hostname: localhost
Server Port: 8080
Document Path: /test
Document Length: 2 bytes
Concurrency Level: 10
Time taken for tests: 10.005 seconds
Complete requests: 24706
Failed requests: 0
Total transferred: 3311006 bytes
HTML transferred: 49418 bytes
Requests per second: 2469.42 [#/sec] (mean)
Time per request: 4.050 [ms] (mean)
Time per request: 0.405 [ms] (mean, across all concurrent requests)
Transfer rate: 323.19 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 1.4 0 16
Processing: 0 4 7.6 0 323
Waiting: 0 3 6.9 0 323
Total: 0 4 7.6 0 323
Percentage of the requests served within a certain time (ms)
50% 0
66% 2
75% 8
80% 8
90% 10
95% 16
98% 16
99% 16
100% 323 (longest request)

限流之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Finished 545 requests
Server Software:
Server Hostname: localhost
Server Port: 8080
Document Path: /test
Document Length: 2 bytes
Concurrency Level: 10
Time taken for tests: 10.007 seconds
Complete requests: 545
Failed requests: 0
Total transferred: 73030 bytes
HTML transferred: 1090 bytes
Requests per second: 54.46 [#/sec] (mean)
Time per request: 183.621 [ms] (mean)
Time per request: 18.362 [ms] (mean, across all concurrent requests)
Transfer rate: 7.13 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 1.1 0 16
Processing: 0 179 57.0 199 211
Waiting: 0 178 57.6 198 211
Total: 0 179 56.9 199 211
Percentage of the requests served within a certain time (ms)
50% 199
66% 200
75% 200
80% 200
90% 201
95% 201
98% 202
99% 203
100% 211 (longest request)

多线程互斥

悲观互斥

互斥实际是悲观锁的思想

例如,有下面取款的需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end-start)/1000_000 + " ms");
}
}

用互斥来保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AccountSync implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
synchronized (this) {
return this.balance;
}
}
@Override
public void withdraw(Integer amount) {
synchronized (this) {
this.balance -= amount;
}
}
}

乐观重试

另外一种是乐观锁思想,它其实不是互斥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(int balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while(true) {
// 获取余额的最新值
int prev = balance.get();
// 要修改的余额
int next = prev - amount;
// 真正修改
if(balance.compareAndSet(prev, next)) {
break;
}
}
}
}

同步与异步

等待结果

这时既可以使用同步处理,也可以使用异步来处理

image-20230108195041938

案例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Slf4j(topic = "c.Test17")
public class Test17 {

// 多线程下 join 执行步骤
static int r = 0;
static int r1 = 0;
static int r2 = 0;

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("t1-start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
log.debug("t2-start");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t2.join(); // t2 sleep 2s
log.debug("t2 join end");
t1.join(); // t1 sleep 1s --t2跑完后,t1 已经结束了
log.debug("t1 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}

结果分析

1
2
3
4
5
6
19:53:52.961 c.Test17 [main] - join begin
19:53:52.961 c.Test17 [Thread-0] - t1-start
19:53:52.961 c.Test17 [Thread-1] - t2-start
19:53:54.979 c.Test17 [main] - t2 join end
19:53:54.979 c.Test17 [main] - t1 join end
19:53:54.979 c.Test17 [main] - r1: 10 r2: 20 cost: 2022ms

两个线程同时启动,等待 t2 跑完后,t1 已经结束了

不论是 t1 还是 t2 先 join

image-20230108195838134

都只需要等待 2 s

Join实现同步
1
2
3
4
5
6
7
8
9
10
11
12
13
static int result = 0;
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
result = 10;
}, "t1");
t1.start();
t1.join();
log.debug("结果为:{}", result);
}

输出

1
2
3
4
20:30:40.453 [main] c.TestJoin - 开始
20:30:40.541 [Thread-0] c.TestJoin - 开始
20:30:41.543 [Thread-0] c.TestJoin - 结束
20:30:41.551 [main] c.TestJoin - 结果为:10

评价

  • 需要外部共享变量,不符合面向对象封装的思想
  • 必须等待线程结束,不能配合线程池使用
Future实现同步
1
2
3
4
5
6
7
8
9
10
11
private static void test2() throws InterruptedException, ExecutionException {
log.debug("开始");
FutureTask<Integer> result = new FutureTask<>(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
return 10;
});
new Thread(result, "t1").start();
log.debug("结果为:{}", result.get());
}

输出

1
2
3
4
10:11:57.880 c.TestSync [main] - 开始
10:11:57.942 c.TestSync [t1] - 开始
10:11:58.943 c.TestSync [t1] - 结束
10:11:58.943 c.TestSync [main] - 结果为:10

评价

  • 规避了使用 join 之前的缺点
  • 可以方便配合线程池使用
1
2
3
4
5
6
7
8
9
10
11
12
private static void test3() throws InterruptedException, ExecutionException {
ExecutorService service = Executors.newFixedThreadPool(1);
log.debug("开始");
Future<Integer> result = service.submit(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
return 10;
});
log.debug("结果为:{}, result 的类型:{}", result.get(), result.getClass());
service.shutdown();
}

输出

1
2
3
4
10:17:40.090 c.TestSync [main] - 开始
10:17:40.150 c.TestSync [pool-1-thread-1] - 开始
10:17:41.151 c.TestSync [pool-1-thread-1] - 结束
10:17:41.151 c.TestSync [main] - 结果为:10, result 的类型:class java.util.concurrent.FutureTask

评价

  • 仍然是 main 线程接收结果
  • get 方法是让调用线程同步等待
自定义实现同步

见模式篇:保护性暂停模式

CompletableFuture 实现异步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void test4() {
// 进行计算的线程池
ExecutorService computeService = Executors.newFixedThreadPool(1);
// 接收结果的线程池
ExecutorService resultService = Executors.newFixedThreadPool(1);
log.debug("开始");
CompletableFuture.supplyAsync(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
return 10;
}, computeService).thenAcceptAsync((result) -> {
log.debug("结果为:{}", result);
}, resultService);
}

输出

1
2
3
4
10:36:28.114 c.TestSync [main] - 开始
10:36:28.164 c.TestSync [pool-1-thread-1] - 开始
10:36:29.165 c.TestSync [pool-1-thread-1] - 结束
10:36:29.165 c.TestSync [pool-2-thread-1] - 结果为:10

评价

  • 可以让调用线程异步处理结果,实际是其他线程去同步等待
  • 可以方便地分离不同职责的线程池
  • 以任务为中心,而不是以线程为中心
BlockingQueue 实现异步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void test6() {
ExecutorService consumer = Executors.newFixedThreadPool(1);
ExecutorService producer = Executors.newFixedThreadPool(1);
BlockingQueue<Integer> queue = new SynchronousQueue<>();
log.debug("开始");
producer.submit(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
try {
queue.put(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumer.submit(() -> {
try {
Integer result = queue.take();
log.debug("结果为:{}", result);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

不等待结果

不等待结果通常使用异步来处理

普通线程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j(topic = "c.FileReader")
public class FileReader {
public static void read(String filename) {
int idx = filename.lastIndexOf(File.separator);
String shortName = filename.substring(idx + 1);
try (FileInputStream in = new FileInputStream(filename)) {
long start = System.currentTimeMillis();
log.debug("read [{}] start ...", shortName);
byte[] buf = new byte[1024];
int n = -1;
do {
n = in.read(buf);
} while (n != -1);
long end = System.currentTimeMillis();
log.debug("read [{}] end ... cost: {} ms", shortName, end - start);
} catch (IOException e) {
e.printStackTrace();
}
}
}

没有用线程时,方法的调用是同步的:

1
2
3
4
5
6
7
8
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) {
String fullPath = "E:\\1.mp4";
FileReader.read(fullPath);
log.debug("do other things ...");
}
}

输出

1
2
3
18:39:15 [main] c.FileReader - read [1.mp4] start ...
18:39:19 [main] c.FileReader - read [1.mp4] end ... cost: 4090 ms
18:39:19 [main] c.Sync - do other things ...

使用了线程后,方法的调用时异步的:

1
2
3
4
private static void test1() {
new Thread(() -> FileReader.read(Constants.MP4_FULL_PATH)).start();
log.debug("do other things ...");
}

输出

1
2
3
18:41:53 [main] c.Async - do other things ...
18:41:53 [Thread-0] c.FileReader - read [1.mp4] start ...
18:41:57 [Thread-0] c.FileReader - read [1.mp4] end ... cost: 4197 ms
线程池实现
1
2
3
4
5
6
private static void test2() {
ExecutorService service = Executors.newFixedThreadPool(1);
service.execute(() -> FileReader.read(Constants.MP4_FULL_PATH));
log.debug("do other things ...");
service.shutdown();
}

输出

1
2
3
11:03:31.245 c.TestAsyc [main] - do other things ...
11:03:31.245 c.FileReader [pool-1-thread-1] - read [1.mp4] start ...
11:03:33.479 c.FileReader [pool-1-thread-1] - read [1.mp4] end ... cost: 2235 ms
CompletableFuture 实现
1
2
3
4
5
private static void test3() throws IOException {
CompletableFuture.runAsync(() -> FileReader.read(Constants.MP4_FULL_PATH));
log.debug("do other things ...");
System.in.read();
}

输出

1
2
3
11:09:38.145 c.TestAsyc [main] - do other things ...
11:09:38.145 c.FileReader [ForkJoinPool.commonPool-worker-1] - read [1.mp4] start ...
11:09:40.514 c.FileReader [ForkJoinPool.commonPool-worker-1] - read [1.mp4] end ... cost: 2369 ms

缓存实现

缓存更新策略

更新时,是先清缓存还是先更新数据库

先清缓存

image-20230114010524357

先更新数据库

image-20230114010559327

补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询

image-20230114010628898

这种情况的出现几率非常小,见 facebook 论文

读写锁实现一致性缓存

使用读写锁实现一个简单的按需加载缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class GenericCachedDao<T> {
// HashMap 作为缓存非线程安全, 需要保护
HashMap<SqlPair, T> map = new HashMap<>();

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
GenericDao genericDao = new GenericDao();
public int update(String sql, Object... params) {
SqlPair key = new SqlPair(sql, params);
// 加写锁, 防止其它线程对缓存读取和更改
lock.writeLock().lock();
try {
int rows = genericDao.update(sql, params);
map.clear();
return rows;
} finally {
lock.writeLock().unlock();
}
}
public T queryOne(Class<T> beanClass, String sql, Object... params) {
SqlPair key = new SqlPair(sql, params);
// 加读锁, 防止其它线程对缓存更改
lock.readLock().lock();
try {
T value = map.get(key);
if (value != null) {
return value;
}
} finally {
lock.readLock().unlock();
}
// 加写锁, 防止其它线程对缓存读取和更改
lock.writeLock().lock();
try {
// get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据
// 为防止重复查询数据库, 再次验证
T value = map.get(key);
if (value == null) {
// 如果没有, 查询数据库
value = genericDao.queryOne(beanClass, sql, params);
map.put(key, value);
}
return value;
} finally {
lock.writeLock().unlock();
}
}
// 作为 key 保证其是不可变的
class SqlPair {
private String sql;
private Object[] params;
public SqlPair(String sql, Object[] params) {
this.sql = sql;
this.params = params;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SqlPair sqlPair = (SqlPair) o;
return sql.equals(sqlPair.sql) &&
Arrays.equals(params, sqlPair.params);
}
@Override
public int hashCode() {
int result = Objects.hash(sql);
result = 31 * result + Arrays.hashCode(params);
return result;
}
}
}

注意

  • 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
    • 适合读多写少,如果写操作比较频繁,以上实现性能低
    • 没有考虑缓存容量
    • 没有考虑缓存过期
    • 只适合单机
    • 并发性还是低,目前只会用一把锁
    • 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
  • 乐观锁实现:用 CAS 去更新

分治策略

案例 - 单词计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static <V> void demo(Supplier<Map<String, V>> supplier, BiConsumer<Map<String, V>,
List<String>> consumer) {
Map<String, V> counterMap = supplier.get();
List<Thread> ts = new ArrayList<>();
for (int i = 1; i <= 26; i++) {
int idx = i;
Thread thread = new Thread(() -> {
List<String> words = readFromFile(idx);
consumer.accept(counterMap, words);
});
ts.add(thread);
}
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(counterMap);
}
public static List<String> readFromFile(int i) {
ArrayList<String> words = new ArrayList<>();
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/"
+ i + ".txt")))) {
while (true) {
String word = in.readLine();
if (word == null) {
break;
}
words.add(word);
}
return words;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

解法1:

1
2
3
4
5
6
7
8
demo(
() -> new ConcurrentHashMap<String, LongAdder>(),
(map, words) -> {
for (String word : words) {
map.computeIfAbsent(word, (key) -> new LongAdder()).increment();
}
}
);

解法2:

1
2
3
4
Map<String, Integer> collect = IntStream.range(1, 27).parallel()
.mapToObj(idx -> readFromFile(idx))
.flatMap(list -> list.stream())
.collect(Collectors.groupingBy(Function.identity(), Collectors.summingInt(w -> 1)));

案例 - 求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class AddTask3 extends RecursiveTask<Integer> {

int begin;
int end;

public AddTask3(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
// 5, 5
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
// 4, 5
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}

// 1 5
int mid = (end + begin) / 2; // 3
AddTask3 t1 = new AddTask3(begin, mid); // 1,3
t1.fork();
AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5
t2.fork();
log.debug("fork() {} + {} = ?", t1, t2);
int result = t1.join() + t2.join();
log.debug("join() {} + {} = {}", t1, t2, result);
return result;
}
}

然后提交给 ForkJoinPool 来执行

1
2
3
4
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask3(1, 10)));
}

结果

1
2
3
4
5
6
7
8
[ForkJoinPool-1-worker-0] - join() 1 + 2 = 3
[ForkJoinPool-1-worker-3] - join() 4 + 5 = 9
[ForkJoinPool-1-worker-0] - join() 3
[ForkJoinPool-1-worker-1] - fork() {1,3} + {4,5} = ?
[ForkJoinPool-1-worker-2] - fork() {1,2} + {3,3} = ?
[ForkJoinPool-1-worker-2] - join() {1,2} + {3,3} = 6
[ForkJoinPool-1-worker-1] - join() {1,3} + {4,5} = 15
15

统筹策略

线程的统筹应用

案例 -烧开水泡茶

解法1:join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Thread t1 = new Thread(() -> {
log.debug("洗水壶");
sleep(1);
log.debug("烧开水");
sleep(15);
}, "老王");
Thread t2 = new Thread(() -> {
log.debug("洗茶壶");
sleep(1);
log.debug("洗茶杯");
sleep(2);
log.debug("拿茶叶");
sleep(1);
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("泡茶");
}, "小王");
t1.start();
t2.start();

输出

1
2
3
4
5
6
19:19:37.547 [小王] c.TestMakeTea - 洗茶壶
19:19:37.547 [老王] c.TestMakeTea - 洗水壶
19:19:38.552 [小王] c.TestMakeTea - 洗茶杯
19:19:38.552 [老王] c.TestMakeTea - 烧开水
19:19:40.553 [小王] c.TestMakeTea - 拿茶叶
19:19:53.553 [小王] c.TestMakeTea - 泡茶

解法1 的缺陷:

  • 上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿来了,老王泡茶呢?代码最好能适应两种情况
  • 上面的两个线程其实是各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡茶呢

解法2:wait/notify

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class S2 {
static String kettle = "冷水";
static String tea = null;
static final Object lock = new Object();
static boolean maked = false;
public static void makeTea() {
new Thread(() -> {
log.debug("洗水壶");
sleep(1);
log.debug("烧开水");
sleep(5);
synchronized (lock) {
kettle = "开水";
lock.notifyAll();
while (tea == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!maked) {
log.debug("拿({})泡({})", kettle, tea);
maked = true;
}
}
}, "老王").start();
new Thread(() -> {
log.debug("洗茶壶");
sleep(1);
log.debug("洗茶杯");
sleep(2);
log.debug("拿茶叶");
sleep(1);
synchronized (lock) {
tea = "花茶";
lock.notifyAll();
while (kettle.equals("冷水")) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!maked) {
log.debug("拿({})泡({})", kettle, tea);
maked = true;
}
}
}, "小王").start();
}
}

输出

1
2
3
4
5
6
20:04:48.179 c.S2 [小王] - 洗茶壶
20:04:48.179 c.S2 [老王] - 洗水壶
20:04:49.185 c.S2 [老王] - 烧开水
20:04:49.185 c.S2 [小王] - 洗茶杯
20:04:51.185 c.S2 [小王] - 拿茶叶
20:04:54.185 c.S2 [老王] - 拿(开水)泡(花茶)

解法2 解决了解法1 的问题,不过老王和小王需要相互等待,不如他们只负责各自的任务,泡茶交给第三人来做

解法3:第三者协调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class S3 {
static String kettle = "冷水";
static String tea = null;
static final Object lock = new Object();
public static void makeTea() {
new Thread(() -> {
log.debug("洗水壶");
sleep(1);
log.debug("烧开水");
sleep(5);
synchronized (lock) {
kettle = "开水";
lock.notifyAll();
}
}, "老王").start();
new Thread(() -> {
log.debug("洗茶壶");
sleep(1);
log.debug("洗茶杯");
sleep(2);
log.debug("拿茶叶");
sleep(1);
synchronized (lock) {
tea = "花茶";
lock.notifyAll();
}
}, "小王").start();
new Thread(() -> {
synchronized (lock) {
while (kettle.equals("冷水") || tea == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("拿({})泡({})", kettle, tea);
}
}, "王夫人").start();
}
}

输出

1
2
3
4
5
6
20:13:18.202 c.S3 [小王] - 洗茶壶
20:13:18.202 c.S3 [老王] - 洗水壶
20:13:19.206 c.S3 [小王] - 洗茶杯
20:13:19.206 c.S3 [老王] - 烧开水
20:13:21.206 c.S3 [小王] - 拿茶叶
20:13:24.207 c.S3 [王夫人] - 拿(开水)泡(花茶)

定时

定期执行

如何让每周四 18:00:00 定时执行任务?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获得当前时间
LocalDateTime now = LocalDateTime.now();
// 获取本周四 18:00:00.000
LocalDateTime thursday =
now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);
// 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000
if(now.compareTo(thursday) >= 0) {
thursday = thursday.plusWeeks(1);
}
// 计算时间差,即延时执行时间
long initialDelay = Duration.between(now, thursday).toMillis();
// 计算间隔时间,即 1 周的毫秒值
long oneWeek = 7 * 24 * 3600 * 1000;
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
System.out.println("开始时间:" + new Date());
executor.scheduleAtFixedRate(() -> {
System.out.println("执行时间:" + new Date());
}, initialDelay, oneWeek, TimeUnit.MILLISECONDS);

原理

指令级并行原理

CPU –> 流水线工作原理

概念名词

Clock Cycle Time

主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的 Cycle Time 是 1s

例如,运行一条加法指令一般需要一个时钟周期时间

CPI

有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数

IPC

IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数

CPU 执行时间

程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示

$程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time $

问题引入

加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工…

image-20230110122844314可以将每个鱼罐头的加工流程细分为 5 个步骤:

  • 去鳞清洗 10分钟
  • 蒸煮沥水 10分钟
  • 加注汤料 10分钟
  • 杀菌出锅 10分钟
  • 真空封罐 10分钟
image-20230110122937992

即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会 影响对第二条鱼的杀菌出锅…

指令重排序优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据 写回 这 5 个阶段

image-20230110123057136

术语参考:

  • instruction fetch (IF)
  • instruction decode (ID)
  • execute (EX)
  • memory access (MEM)
  • register write back (WB)

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序组合来实现指令级并行,这一技术在 80’s 中叶到 90’s 中叶占据了计算架构的重要地位。

提示:

分阶段,分工是提升效率的关键!

指令重排的前提是,重排指令不能影响结果,例如

1
2
3
4
5
6
7
8
// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );

// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2

参考文档

参考: Scoreboarding and the Tomasulo algorithm (which is similar to scoreboarding but makes use of register renaming) are two of the most common techniques for implementing out-of-order execution and instruction-level parallelism.

支持流水线的处理器

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一 条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了 指令地吞吐率。

提示:

奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃

image-20230110123515027

SuperScalar 处理器

大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC > 1

image-20230110123601575 image-20230110123619884

返回有序性

CPU缓存结构原理

CPU 缓存结构

image-20230114003502367

查看 cpu 缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
⚡ root@yihang01 ~ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 1
On-line CPU(s) list: 0
Thread(s) per core: 1
Core(s) per socket: 1
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 142
Model name: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
Stepping: 11
CPU MHz: 1992.002
BogoMIPS: 3984.00
Hypervisor vendor: VMware
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 8192K
NUMA node0 CPU(s): 0

速度比较

从 cpu 到 大约需要的时钟周期
寄存器 1 cycle
L1 3~4 cycle
L2 10~20 cycle
L3 40~45 cycle
内存 120~240 cycle

查看 cpu 缓存行

1
⚡ root@yihang01 ~ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size 64

cpu 拿到的内存地址格式是这样的

1
[高位组标记][低位索引][偏移量]
image-20230114003711579

CPU 缓存读

读取数据流程如下

  • 根据低位,计算在缓存中的索引
  • 判断是否有效
    • 0 去内存读取新数据更新缓存行
    • 1 再对比高位组标记是否一致
      • 一致,根据偏移量返回缓存数据
      • 不一致,去内存读取新数据更新缓存行

CPU 缓存一致性

MESI 协议

  1. E、S、M 状态的缓存行都可以满足 CPU 的读请求
  2. E 状态的缓存行,有写请求,会将状态改为 M,这时并不触发向主存的写
  3. E 状态的缓存行,必须监听该缓存行的读操作,如果有,要变为 S 状态
image-20230114003900444
  1. M 状态的缓存行,必须监听该缓存行的读操作,如果有,先将其它缓存(S 状态)中该缓存行变成 I 状态(即 6. 的流程),写入主存,自己变为 S 状态
  2. S 状态的缓存行,有写请求,走 4. 的流程
  3. S 状态的缓存行,必须监听该缓存行的失效操作,如果有,自己变为 I 状态
  4. I 状态的缓存行,有读请求,必须从主存读取
image-20230114004021538

内存屏障

Memory Barrier(Memory Fence)

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
image-20230114004125640

Monitor操作系统层原理

Monitor 被翻译为监视器管程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针 Monitor

图解结构如下

image-20230109133048037 image-20230109134132025 image-20230109134211110
  • 刚开始 MonitorOwnernull
  • Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2Monitor只能有一个 Owner
  • Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 **synchronized(obj)**,就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,此时的竞争是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

返回Monitor概念

Synchronized字节码原理

1
2
3
4
5
6
7
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}

对应的字节码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // <- lock引用 (synchronized开始)
3: dup
4: astore_1 // lock引用 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // <- i
9: iconst_1 // 准备常数 1
10: iadd // +1
11: putstatic #3 // -> i
14: aload_1 // <- lock引用
15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16: goto 24
19: astore_2 // e -> slot 2 --> 处理异常
20: aload_1 // <- lock引用
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // <- slot 2 (e)
23: athrow // throw e
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4

Synchronized原理进阶与优化

返回Synchronized锁优化

轻量级锁

轻量级锁理论

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争,一旦有锁竞争就会升级成重量级锁),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized

1
2
3
4
5
6
7
8
9
10
11
12
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}

图解轻量级锁

image-20230109151402264 image-20230109152350221 image-20230109152700405 image-20230109152722790 image-20230109152738689
  • 每个对象 Object 由对象头和对象体组成。对象头中,存有 Hashcode Age Bias 01,KlassWord 存储类型指针对象体在堆内存中的引用,Object body储存成员变量等信息
  • 创建锁记录(Lock Record)对象,线程的栈帧都会包含一个锁记录的结构,内部 lockrecord地址-00 存储锁定对象的 Mark Word。Object reference对象指针,记录加锁对象的引用地址
  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录,即 Object 中的 MarkWord锁记录中的 lockrecord地址-00 交换
  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁。锁记录中存储 Object 的 MarkWord
  • 如果 cas 失败,则有两种情况
    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀已经升级为重量级锁,进入重量级锁解锁流程

总结

  • Thread 1 创建对象 Object –> Object 偏向锁偏向于 Thread 1
  • synchronized加锁 –> 轻量级锁 –> (再次加锁 –> 锁重入)
  • Thread 2 访问对象 Object 产生锁竞争 –> 锁膨胀 –> 重量级锁

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

1
2
3
4
5
6
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}

图解锁膨胀

image-20230109154921688 image-20230109154950252
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁 Monitor 地址
    • 然后自己进入 MonitorEntryList BLOCKED

自旋锁优化

重量级锁竞争的时候,并不是直接阻塞,而是使用自旋(循环)来进行优化,如果当前线程自旋成功(–即重量级锁竞争时,持续重试一段时间,如果这段时间内持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

自旋重试成功的情况

线程 1 (core 1 上) 对象 Mark 线程 2 (core 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
- 10(重量锁)重量锁指针 成功(加锁)
- 10(重量锁)重量锁指针 执行同步块
-

自旋重试失败的情况

线程 1 (core 1 上) 对象 Mark 线程 2 (core 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞
-

总结

  • 自旋会占用 CPU 时间单核 CPU 自旋就是浪费多核 CPU 自旋才能发挥优势
  • Java6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能
  • Java7 之后不能控制是否开启自旋功能

偏向锁

偏向锁机制

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}

图解对比轻量级锁与偏向锁

image-20230109163518825 image-20230109163551806
偏向锁状态

偏向锁状态

对象头格式:

image-20230109163957884

创建对象时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

代码测试

利用 jol 第三方工具来查看对象头信息(注意这里我扩展了 jol 让它输出更为简洁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Slf4j(topic = "c.Test1")
public class Test1 {
// 测试默认开启偏向锁
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();

// 001 偏向锁默认有延迟开启,在 jvm 内部延迟未结束之前,创建的对象默认都是 001-正常状态
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());

TimeUnit.SECONDS.sleep(4);

// 101 在 jvm 内部延迟结束后,创建的对象都是 101 默认开启延迟
log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple());
log.debug("-------------------------");
// 已创建的对象在延迟结束后是不会更改成 101 的
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
}

// 不开启延迟禁用
// 15:44:48.387 c.Test1 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (non-biasable; age: 0)
// 15:44:52.401 c.Test1 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 (biasable; age: 0)
// 15:44:52.401 c.Test1 [main] - -------------------------
// 15:44:52.401 c.Test1 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (non-biasable; age: 0)

// 开启延迟禁用
// 15:44:48.387 c.Test1 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (non-biasable; age: 0)
// 15:44:52.401 c.Test1 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 (biasable; age: 0)
// 15:44:52.401 c.Test1 [main] - -------------------------
// 15:44:52.401 c.Test1 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (non-biasable; age: 0)
}

class Dog {
}
偏向锁升级

偏向锁升级轻量级锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j(topic = "c.Test2")
public class Test2 {
// 测试 加锁后 线程ID 写入 MarkWork
public static void main(String[] args) {
Dog dog = new Dog();
// 打印设置无延迟 偏向锁
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());

synchronized (dog) {
// 加锁后 对象MarkWord 偏向锁机制
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
}

// 解锁后 对象MarkWord 偏向锁机制
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
// 开启延迟禁用 -XX:BiasedLockingStartupDelay=0
// 03:27:47.316 c.Test2 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 (biasable; age: 0)
// 03:27:47.317 c.Test2 [main] - 00000000 00000000 00000000 00000000 00000010 11001100 00111000 00000101 (biased: 0x000000000000b30e; epoch: 0; age: 0)
// 03:27:47.318 c.Test2 [main] - 00000000 00000000 00000000 00000000 00000010 11001100 00111000 00000101 (biased: 0x000000000000b30e; epoch: 0; age: 0)
}
}

在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

禁用偏向锁,直接使用 轻量级锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j(topic = "c.Test3")
public class Test3 {
// 测试 禁用偏向锁 -XX:-UseBiasedLocking
// -XX:+UseBiasedLocking 开启
public static void main(String[] args) {
Dog dog = new Dog();
// 打印设置禁用偏向锁 直接使用轻量级锁
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());

synchronized (dog) {
// 加锁后 对象MarkWord 轻量级锁机制
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
}

// 解锁后 对象MarkWord 轻量级锁机制
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());

// 轻量级锁 每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
// 03:40:38.366 c.Test3 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (non-biasable; age: 0)
// 03:40:38.368 c.Test3 [main] - 00000000 00000000 00000000 00000000 00000011 01001000 11110100 10111000 (thin lock: 0x000000000348f4b8)
// 03:40:38.368 c.Test3 [main] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (non-biasable; age: 0)

}
}

注意

  • 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
  • 正常状态对象一开始是没有 hashCode 的,第一次调用才生成
偏向锁撤销

偏向锁撤销

  1. 调用了对象的 hashCode 就会撤销偏向锁。偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
    • 轻量级锁不撤除,thin lock 会在栈帧锁记录中记录 hashCode
    • 重量级锁不撤除,heavy lock 会在 Monitor 中记录 hashCode
  2. 有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  3. 调用 wait/notify(只用重量级锁才能使用),会将偏向锁升级为重量级锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j(topic = "c.Test4")
public class Test4 {
// 测试 调用 hashcode 撤销对象偏向锁
public static void main(String[] args) {
Dog dog = new Dog();
// hashcode 同样是懒加载机制,只有在第一次调用 hashcode 时才会产生,默认都使 0
// 当第一次调用对象的hashcode时,才会产生 hash码,然后填充到对象的 MarkWord 里
// 因为产生 hashcode 与 偏向锁在 MarkWord 的信息字段存储上冲突了,所以当调用对象的hashcode时
// 会自动覆盖导致不能在 MarkWord 中填充线程ID,从而直接锁膨胀成轻量级锁
dog.hashCode();
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());

synchronized (dog) {
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
}

log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
// 关闭延迟
// 03:47:44.530 c.Test4 [main] - 00000000 00000000 00000000 01010110 01111101 00101001 10011011 00000001 (hash: 0x567d299b; age: 0)
// 03:47:44.531 c.Test4 [main] - 00000000 00000000 00000000 00000000 00000010 10000010 11110100 11111000 (thin lock: 0x000000000282f4f8)
// 03:47:44.531 c.Test4 [main] - 00000000 00000000 00000000 01010110 01111101 00101001 10011011 00000001 (hash: 0x567d299b; age: 0)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j(topic = "c.Test5")
public class Test5 {
// 测试 线程交错使用对象,导致撤销偏向锁,升级成轻量级锁
public static void main(String[] args) {

Dog dog = new Dog();

new Thread(() -> {
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
synchronized (dog) {
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
}
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());

// wait-notify 机制实现线程交错
synchronized (Test5.class) {
Test5.class.notify();
}
}, "t1").start();

new Thread(() -> {
synchronized (Test5.class) {
try {
Test5.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
synchronized (dog) {
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());
}
log.debug(ClassLayout.parseInstance(dog).toPrintableSimple());

}, "t2").start();

// 04:00:24.740 c.Test5 [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 (biasable; age: 0)
// 04:00:24.742 c.Test5 [t1] - 00000000 00000000 00000000 00000000 00011111 11101110 11111000 00000101 (biased: 0x000000000007fbbe; epoch: 0; age: 0)
// 04:00:24.742 c.Test5 [t1] - 00000000 00000000 00000000 00000000 00011111 11101110 11111000 00000101 (biased: 0x000000000007fbbe; epoch: 0; age: 0)
// 04:00:24.743 c.Test5 [t2] - 00000000 00000000 00000000 00000000 00011111 11101110 11111000 00000101 (biased: 0x000000000007fbbe; epoch: 0; age: 0)
// 04:00:24.743 c.Test5 [t2] - 00000000 00000000 00000000 00000000 00100000 01001001 11101111 11010000 (thin lock: 0x000000002049efd0)
// 04:00:24.743 c.Test5 [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 (non-biasable; age: 0)
}
}
批量重偏向

偏向锁-批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID

当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Slf4j(topic = "c.Test6")
public class Test6 {
// 测试 批量重定向 --当对象被多个线程访问,但没有竞争时,
// 如果偏向线程 T1 的对象被线程 T2 使用,重偏向会撤销对 T1 的偏向并重置对象的 Thread ID 偏向 T2。
// 当撤销偏向锁阈值超过 20 次后,JVM 会执行批量重偏向
public static void main(String[] args) {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();

Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
}
}, "t2");
t2.start();
}
}
批量撤销

偏向锁-批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Slf4j(topic = "c.Test7")
public class Test7 {
// 测试 --批量撤销偏向锁 撤销阈值达到 40 会将整个 类 的所有对象都变成 001 不可偏向
// 之后新建的对象也不能偏向
static Thread t1, t2, t3;

public static void main(String[] args) throws InterruptedException {
Vector<Dog> list = new Vector<>();

int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();

t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
}
LockSupport.unpark(t3);
}, "t2");
t2.start();

t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple());
}
}, "t3");
t3.start();

// 不达到第四十个的阈值,所创建的对象都是 101 可偏向对象
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple());
}
}

锁消除

JIT 即时编译器锁消除优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java -jar benchmarks.jar

# 禁用 JIT 即时编译器锁优化
java -XX:-EliminateLocks -jar benchmarks.jar

// 测试 --锁消除 与 TODO 锁粗化
// 使用 benchmarks.jar jar包查看效果 TODO 分析jar包内容
/*
启用 JIT 即时编译器锁优化
# Run complete. Total time: 00:00:20
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.342 0.031 ns/op
c.i.MyBenchmark.b avgt 5 1.352 0.032 ns/op

禁用 JIT 即时编译器锁优化
# Run complete. Total time: 00:00:20
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.396 0.136 ns/op
c.i.MyBenchmark.b avgt 5 16.468 0.421 ns/op

可以看出明显性能差异
*/

锁粗化

TODO

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度

参考文献:

死磕Synchronized底层实现–概论

盘一盘 synchronized

Oracle锁偏向

wait-notify原理

wait-notify需要基于 synchronized 的 Monitor 锁

image-20230109212207616
  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKEDWAITING 的线程都处于阻塞状态不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notifynotifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

返回wait-notify解决方案

join原理

是调用者轮询检查线程 alive 状态

1
t1.join();

等价于下面的代码

1
2
3
4
5
6
synchronized (t1) {
// 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
while (t1.isAlive()) {
t1.wait(0);
}
}

注意

join 体现的是【保护性暂停】模式,请参考之

park-unpark原理

引入类比学习

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex 打个比喻

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
      • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

图解 park -> unpark

image-20230110001330249 image-20230110001431479
  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量阻塞
  4. 设置 _counter = 0
  5. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  6. 唤醒 _cond 条件变量中的 Thread_0
  7. Thread_0 恢复运行
  8. 设置 _counter 为 0

图解 unpark -> park

image-20230110001555950
  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0

返回park-unpark解决方案

volatile原理

volatile 在 JDK 1.5 之后才能生效

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

从而达到保证变量可见性与指令的有序性

如何保证可见性

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
  • 读屏障(lfence)保证在该屏障之后对共享变量的读取,加载的是主存中最新数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}


public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}

图解

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
participant t1 as t1 线程
participant num as num=0
participant ready as volatile ready=false
participant t2 as t2 线程
t1 -->> t1 : num=2
t1 ->> ready : ready=true
Note over t1,ready: 写屏障
Note over num,t2: 读屏障
t2 ->> ready : 读取ready=true
t2 ->> num : 读取num=2

如何保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}


public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}

图解

image-20230110151923818

注意 -> 还是那句话,不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序
image-20230110161105646

double-checked locking

double-checked locking问题

以 double-checked locking 单例模式为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

分析以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

分析:

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:

image-20230110152347815

图解

  1. 关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值
  2. 这时 t1 还未完全将构造方法执行完毕如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例
  3. 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

double-checked locking 解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

字节码上看不出来 volatile 指令的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27: aload_0
28: monitorexit ------------------------> 保证原子性、可见性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
  • 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性

图解

image-20230110153125434

返回有序性分析

CAS原理

ReentrantLock原理

image-20230112202414023

非公平锁实现原理

加锁解锁

先从构造器开始看,默认为非公平锁实现

1
2
3
public ReentrantLock() {
sync = new NonfairSync();
}

NonfairSync 继承自 AQS

没有竞争时

image-20230114005724673

第一个竞争出现时

image-20230112202609012

Thread-1 执行了

  1. CAS 尝试将 state 由 0 改为 1,结果失败
  2. 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
  3. 接下来进入 addWaiter 逻辑,构造 Node 队列
    • 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
    • Node 的创建是懒惰的
    • 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

当前线程进入 acquireQueued 逻辑

  1. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
  2. 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
  3. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
image-20230112203906123
  1. shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
  2. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
  3. 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
image-20230112204102710

再次有多个线程经历上述过程竞争失败,变成这个样子

image-20230113004408600

Thread-0 释放锁,进入 tryRelease 流程,如果成功

  • 设置 exclusiveOwnerThread 为 null
  • state = 0
image-20230113004507427

当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程

找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1

回到 Thread-1 的 acquireQueued 流程

image-20230113004537647

如果加锁成功(没有竞争),会设置

  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,而可被垃圾回收

如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了

image-20230114005827365

如果不巧又被 Thread-4 占了先

  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
加锁源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Sync 继承自 AQS
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

// 加锁实现
final void lock() {
// 首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果尝试失败,进入 ㈠
acquire(1);
}

// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
// ㈡ tryAcquire
if (
!tryAcquire(arg) &&
// 当 tryAcquire 返回为 false 时, 先调用 addWaiter ㈣, 接着 acquireQueued ㈤
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}

// ㈡ 进入 ㈢
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

// ㈢ Sync 继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果还没有获得锁
if (c == 0) {
// 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
else if (current == getExclusiveOwnerThread()) {
// state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 获取失败, 回到调用处
return false;
}

// ㈣ AQS 继承过来的方法, 方便阅读, 放在此处
private Node addWaiter(Node mode) {
// 将当前线程关联到一个 Node 对象上, 模式为独占模式
Node node = new Node(Thread.currentThread(), mode);
// 如果 tail 不为 null, cas 尝试将 Node 对象加入 AQS 队列尾部
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
// 双向链表
pred.next = node;
return node;
}
}
// 尝试将 Node 加入 AQS, 进入 ㈥
enq(node);
return node;
}

// ㈥ AQS 继承过来的方法, 方便阅读, 放在此处
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// 还没有, 设置 head 为哨兵节点(不对应线程,状态为 0)
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
// cas 尝试将 Node 对象加入 AQS 队列尾部
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

// ㈤ AQS 继承过来的方法, 方便阅读, 放在此处
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 上一个节点是 head, 表示轮到自己(当前线程对应的 node)了, 尝试获取
if (p == head && tryAcquire(arg)) {
// 获取成功, 设置自己(当前线程对应的 node)为 head
setHead(node);
// 上一个节点 help GC
p.next = null;
failed = false;
// 返回中断标记 false
return interrupted;
}
if (
// 判断是否应当 park, 进入 ㈦
shouldParkAfterFailedAcquire(p, node) &&
// park 等待, 此时 Node 的状态被置为 Node.SIGNAL ㈧
parkAndCheckInterrupt()
) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}

// ㈦ AQS 继承过来的方法, 方便阅读, 放在此处
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取上一个节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) {
// 上一个节点都在阻塞, 那么自己也阻塞好了
return true;
}
// > 0 表示取消状态
if (ws > 0) {
// 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 这次还没有阻塞
// 但下次如果重试不成功, 则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

// ㈧ 阻塞当前线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
}

注意

  • 是否需要 unpark 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定,而不是本节点的 waitStatus 决定
解锁源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// Sync 继承自 AQS
static final class NonfairSync extends Sync {
// 解锁实现
public void unlock() {
sync.release(1);
}

// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean release(int arg) {
// 尝试释放锁, 进入 ㈠
if (tryRelease(arg)) {
// 队列头节点 unpark
Node h = head;
if (
// 队列不为 null
h != null &&
// waitStatus == Node.SIGNAL 才需要 unpark
h.waitStatus != 0
) {
// unpark AQS 中等待的线程, 进入 ㈡
unparkSuccessor(h);
}
return true;
}
return false;
}

// ㈠ Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
private void unparkSuccessor(Node node) {
// 如果状态为 Node.SIGNAL 尝试重置状态为 0
// 不成功也可以
int ws = node.waitStatus;
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
// 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的
Node s = node.next;
// 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
}

可重入原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static final class NonfairSync extends Sync {
// ...

// Sync 继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
else if (current == getExclusiveOwnerThread()) {
// state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}

可打断原理

不可打断模式

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Sync 继承自 AQS
static final class NonfairSync extends Sync {
// ...

private final boolean parkAndCheckInterrupt() {
// 如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// interrupted 会清除打断标记
return Thread.interrupted();
}

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
// 还是需要获得锁后, 才能返回打断状态
return interrupted;
}
if (
shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()
) {
// 如果是因为 interrupt 被唤醒, 返回打断状态为 true
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}

public final void acquire(int arg) {
if (
!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
// 如果打断状态为 true
selfInterrupt();
}
}

static void selfInterrupt() {
// 重新产生一次中断
Thread.currentThread().interrupt();
}
}
可打断模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static final class NonfairSync extends Sync {
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果没有获得到锁, 进入 ㈠
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

// ㈠ 可打断的获取锁流程
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
// 在 park 过程中如果被 interrupt 会进入此
// 这时候抛出异常, 而不会再次进入 for (;;)
throw new InterruptedException();
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}

公平锁实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}

// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (
!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}
// 与非公平锁主要区别在于 tryAcquire 方法的实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// h != t 时表示队列中有 Node
return h != t &&
(
// (s = h.next) == null 表示队列中还有没有老二
(s = h.next) == null ||
// 或者队列中老二线程不是此线程
s.thread != Thread.currentThread()
);
}
}

条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

await 流程

开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程

创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

image-20230113005408979

接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁

image-20230113005441332

unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功

image-20230113005502068

park 阻塞 Thread-0

image-20230113005524132
signal 流程

假设 Thread-1 要来唤醒 Thread-0

image-20230113005558716

进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

image-20230113005625326

执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1

image-20230113005651312

Thread-1 释放锁,进入 unlock 流程,略

源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;

// 第一个等待节点
private transient Node firstWaiter;

// 最后一个等待节点
private transient Node lastWaiter;
public ConditionObject() { }
// ㈠ 添加一个 Node 至等待队列
private Node addConditionWaiter() {
Node t = lastWaiter;
// 所有已取消的 Node 从队列链表删除, 见 ㈡
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建一个关联当前线程的新 Node, 添加至队列尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
// 唤醒 - 将没取消的第一个节点转移至 AQS 队列
private void doSignal(Node first) {
do {
// 已经是尾节点了
if ( (firstWaiter = first.nextWaiter) == null) {
lastWaiter = null;
}
first.nextWaiter = null;
} while (
// 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢
!transferForSignal(first) &&
// 队列还有节点
(first = firstWaiter) != null
);
}

// 外部类方法, 方便阅读, 放在此处
// ㈢ 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功
final boolean transferForSignal(Node node) {
// 如果状态已经不是 Node.CONDITION, 说明被取消了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 加入 AQS 队列尾部
Node p = enq(node);
int ws = p.waitStatus;
if (
// 上一个节点被取消
ws > 0 ||
// 上一个节点不能设置状态为 Node.SIGNAL
!compareAndSetWaitStatus(p, ws, Node.SIGNAL)
) {
// unpark 取消阻塞, 让线程重新同步状态
LockSupport.unpark(node.thread);
}
return true;
}
// 全部唤醒 - 等待队列的所有节点转移至 AQS 队列
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}

// ㈡
private void unlinkCancelledWaiters() {
// ...
}
// 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
// 全部唤醒 - 必须持有锁才能唤醒, 因此 doSignalAll 内无需考虑加锁
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
// 不可打断等待 - 直到被唤醒
public final void awaitUninterruptibly() {
// 添加一个 Node 至等待队列, 见 ㈠
Node node = addConditionWaiter();
// 释放节点持有的锁, 见 ㈣
int savedState = fullyRelease(node);
boolean interrupted = false;
// 如果该节点还没有转移至 AQS 队列, 阻塞
while (!isOnSyncQueue(node)) {
// park 阻塞
LockSupport.park(this);
// 如果被打断, 仅设置打断状态
if (Thread.interrupted())
interrupted = true;
}
// 唤醒后, 尝试竞争锁, 如果失败进入 AQS 队列
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt();
}
// 外部类方法, 方便阅读, 放在此处
// ㈣ 因为某线程可能重入,需要将 state 全部释放
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
// 打断模式 - 在退出等待时重新设置打断状态
private static final int REINTERRUPT = 1;
// 打断模式 - 在退出等待时抛出异常
private static final int THROW_IE = -1;
// 判断打断模式
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
// ㈤ 应用打断模式
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
// 等待 - 直到被唤醒或打断
public final void await() throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 添加一个 Node 至等待队列, 见 ㈠
Node node = addConditionWaiter();
// 释放节点持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 如果该节点还没有转移至 AQS 队列, 阻塞
while (!isOnSyncQueue(node)) {
// park 阻塞
LockSupport.park(this);
// 如果被打断, 退出等待队列
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 退出等待队列后, 还需要获得 AQS 队列的锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 所有已取消的 Node 从队列链表删除, 见 ㈡
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 应用打断模式, 见 ㈤
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 等待 - 直到被唤醒或打断或超时
public final long awaitNanos(long nanosTimeout) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 添加一个 Node 至等待队列, 见 ㈠
Node node = addConditionWaiter();
// 释放节点持有的锁
int savedState = fullyRelease(node);
// 获得最后期限
final long deadline = System.nanoTime() + nanosTimeout;
int interruptMode = 0;
// 如果该节点还没有转移至 AQS 队列, 阻塞
while (!isOnSyncQueue(node)) {
// 已超时, 退出等待队列
if (nanosTimeout <= 0L) {
transferAfterCancelledWait(node);
break;
}
// park 阻塞一定时间, spinForTimeoutThreshold 为 1000 ns
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 如果被打断, 退出等待队列
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
nanosTimeout = deadline - System.nanoTime();
}
// 退出等待队列后, 还需要获得 AQS 队列的锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 所有已取消的 Node 从队列链表删除, 见 ㈡
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 应用打断模式, 见 ㈤
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return deadline - System.nanoTime();
}
// 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanos
public final boolean awaitUntil(Date deadline) throws InterruptedException {
// ...
}
// 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanos
public final boolean await(long time, TimeUnit unit) throws InterruptedException {
// ...
}
// 工具方法 省略 ...
}

返回ReentrantLock解决方案

final原理

设置 final 变量原理

理解了 volatile 原理,再对比 final 的实现就比较简单了

1
2
3
public class TestFinal {
final int a = 20;
}

字节码

1
2
3
4
5
6
7
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
<-- 写屏障
10: return

发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况

获取 final 变量原理

AQS原理

AQS概述

全称是 AbstractQueuedSynchronizer,是阻塞式锁相关的同步器工具框架–其他的阻塞式锁与同步器都是他的子类

AQS特点

  • 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
    • getState - 获取 state 状态
    • setState - 设置 state 状态
    • compareAndSetState - cas 机制设置 state 状态
    • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

AQS操作

获取锁

1
2
3
4
// 如果获取锁失败
if (!tryAcquire(arg)) {
// 入队, 可以选择阻塞当前线程 park unpark
}

释放锁

1
2
3
4
// 如果释放锁成功
if (tryRelease(arg)) {
// 让阻塞线程恢复运行
}

AQS实现锁

AQS实现不可重入锁

==自定义同步器==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class MySync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int acquires) {
if (acquires == 1){
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
@Override
protected boolean tryRelease(int acquires) {
if(acquires == 1) {
if(getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
return false;
}
protected Condition newCondition() {
return new ConditionObject();
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}

==自定义锁==

有了自定义同步器,很容易复用 AQS ,实现一个功能完备的自定义锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyLock implements Lock {
static MySync sync = new MySync();
@Override
// 尝试,不成功,进入等待队列
public void lock() {
sync.acquire(1);
}
@Override
// 尝试,不成功,进入等待队列,可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
// 尝试一次,不成功返回,不进入队列
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
// 尝试,不成功,进入等待队列,有时限
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
// 释放锁
public void unlock() {
sync.release(1);
}
@Override
// 生成条件变量
public Condition newCondition() {
return sync.newCondition();
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MyLock lock = new MyLock();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
sleep(1);
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t2").start();

输出

1
2
3
4
22:29:28.727 c.TestAqs [t1] - locking...
22:29:29.732 c.TestAqs [t1] - unlocking...
22:29:29.732 c.TestAqs [t2] - locking...
22:29:29.732 c.TestAqs [t2] - unlocking...

不可重入测试

如果改为下面代码,会发现自己也会被挡住(只会打印一次 locking)

1
2
3
4
lock.lock();
log.debug("locking...");
lock.lock();
log.debug("locking...");

总结

==起源==

  • 早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不够优雅,于是在 JSR166(java 规范提案)中创建了 AQS,提供了这种通用的同步器机制。

==目标== –AQS 要实现的功能目标

  • 阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
  • 获取锁超时机制
  • 通过打断取消机制
  • 独占机制及共享机制
  • 条件不满足时的等待机制

要实现的性能目标

Instead, the primary performance goal here is scalability: to predictably maintain efficiency even, or especially, when synchronizers are contended.

==设计== –AQS 的基本思想其实很简单

获取锁的逻辑

1
2
3
4
5
6
while(state 状态不允许获取) {
if(队列中还没有此线程) {
入队并阻塞
}
}
当前线程出队

释放锁的逻辑

1
2
3
if(state 状态允许了) {
恢复阻塞的线程(s)
}

核心

  • 原子维护 state 状态
  • 阻塞及恢复线程
  • 维护队列

设计方案

  1. state 设计
    • state 使用 volatile 配合 cas 保证其修改时的原子性
    • state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
  2. 阻塞恢复设计
    • 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到
    • 解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没问题
    • park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
    • park 线程还可以通过 interrupt 打断
  3. 队列设计
    • 使用了 FIFO 先入先出队列,并不支持优先级队列
    • 设计时借鉴了 CLH 队列,它是一种单向无锁队列
image-20230112181910356

队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有 state 维护节点状态入队伪代码,只需要考虑 tail 赋值的原子性

1
2
3
4
5
do {
// 原来的 tail
Node prev = tail;
// 用 cas 在原来 tail 的基础上改为 node
} while(tail.compareAndSet(prev, node))

出队伪代码

1
2
3
4
5
// prev 是上一个节点
while((Node prev=node.prev).state != 唤醒状态) {
}
// 设置头节点
head = node;

CLH 优势:

  • 无锁,使用自旋
  • 快速,无阻塞

AQS 在一些方面改进了 CLH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 队列中还没有元素 tail 为 null
if (t == null) {
// 将 head 从 null -> dummy
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 将 node 的 prev 设置为原来的 tail
node.prev = t;
// 将 tail 从原来的 tail 设置为 node
if (compareAndSetTail(t, node)) {
// 原来 tail 的 next 设置为 node
t.next = node;
return t;
}
}
}
}

主要用到 AQS 的并发工具类

image-20230112182118059

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@Slf4j(topic = "c.Test12")
public class Test12 {
// AQS 自定义锁
public static void main(String[] args) {
// 测试 AQS 自定义锁
MyLock lock = new MyLock();
new Thread(() -> {
lock.lock();
// // 测试不可重入
// lock.lock();
try {
log.debug("locking...");
sleep(1);
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t1").start();

new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t2").start();
}
}

// 自定义锁(不可重入锁)
class MyLock implements Lock {

// 独占锁 同步器类
class MySync extends AbstractQueuedSynchronizer {
// 尝试获取锁
@Override // 可重入锁需要使用 arg 参数来进行一些计数
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
// 加上了锁,并设置 owner 为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}

// 尝试释放锁
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}

@Override // 是否持有独占锁
protected boolean isHeldExclusively() {
return getState() == 1;
}

// 返回条件变量
public Condition newCondition() {
return new ConditionObject();
}
}

private MySync sync = new MySync();

@Override // 加锁(不成功会进入等待队列)
public void lock() {
sync.acquire(1);
}

@Override // 加锁,可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

@Override // 尝试加锁(一次)
public boolean tryLock() {
return sync.tryAcquire(1);
}

@Override // 尝试加锁,带超时
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}

@Override // 解锁
public void unlock() {
sync.release(1);
}

@Override // 创建条件变量
public Condition newCondition() {
return sync.newCondition();
}
}

AQS

读写锁原理

图解原理

读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个

t1 w.lock,t2 r.lock
  1. t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
image-20230113023003772
  1. t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败

tryAcquireShared 返回值表示

  • -1 表示失败
  • 0 表示成功,但后继节点不会继续唤醒
  • 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
image-20230113023114330
  1. 这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
image-20230113023145789
  1. t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
  2. 如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
image-20230113023221586
t3 r.lock,t4 w.lock

这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子

image-20230113023300080
t1 w.unlock

这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子

image-20230113023332343

接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行

这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一

image-20230113023413380

这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

image-20230113023448070

事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行

image-20230113023513841

这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一

image-20230113023534233

这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

image-20230113023615872

下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

t2 r.unlock,t3 r.unlock

t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零

image-20230113023651645

t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即

image-20230113023726340

之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束

image-20230113023749874

源码分析

写锁上锁流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
static final class NonfairSync extends Sync {
// ... 省略无关代码

// 外部类 WriteLock 方法, 方便阅读, 放在此处
public void lock() {
sync.acquire(1);
}

// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (
// 尝试获得写锁失败
!tryAcquire(arg) &&
// 将当前线程关联到一个 Node 对象上, 模式为独占模式
// 进入 AQS 队列阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}

// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryAcquire(int acquires) {
// 获得低 16 位, 代表写锁的 state 计数
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);

if (c != 0) {
if (
// c != 0 and w == 0 表示有读锁, 或者
w == 0 ||
// 如果 exclusiveOwnerThread 不是自己
current != getExclusiveOwnerThread()
) {
// 获得锁失败
return false;
}
// 写锁计数超过低 16 位, 报异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入, 获得锁成功
setState(c + acquires);
return true;
}
if (
// 判断写锁是否该阻塞, 或者
writerShouldBlock() ||
// 尝试更改计数失败
!compareAndSetState(c, c + acquires)
) {
// 获得锁失败
return false;
}
// 获得锁成功
setExclusiveOwnerThread(current);
return true;
}

// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
final boolean writerShouldBlock() {
return false;
}
}
写锁释放流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static final class NonfairSync extends Sync {
// ... 省略无关代码

// WriteLock 方法, 方便阅读, 放在此处
public void unlock() {
sync.release(1);
}

// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean release(int arg) {
// 尝试释放写锁成功
if (tryRelease(arg)) {
// unpark AQS 中等待的线程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 因为可重入的原因, 写锁计数为 0, 才算释放成功
boolean free = exclusiveCount(nextc) == 0;
if (free) {
setExclusiveOwnerThread(null);
}
setState(nextc);
return free;
}
}
读锁上锁流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
static final class NonfairSync extends Sync {

// ReadLock 方法, 方便阅读, 放在此处
public void lock() {
sync.acquireShared(1);
}

// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquireShared(int arg) {
// tryAcquireShared 返回负数, 表示获取读锁失败
if (tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}

// Sync 继承过来的方法, 方便阅读, 放在此处
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果是其它线程持有写锁, 获取读锁失败
if (
exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current
) {
return -1;
}
int r = sharedCount(c);
if (
// 读锁不该阻塞(如果老二是写锁,读锁该阻塞), 并且
!readerShouldBlock() &&
// 小于读锁计数, 并且
r < MAX_COUNT &&
// 尝试增加计数成功
compareAndSetState(c, c + SHARED_UNIT)
) {
// ... 省略不重要的代码
return 1;
}
return fullTryAcquireShared(current);
}

// 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁
// true 则该阻塞, false 则不阻塞
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}

// AQS 继承过来的方法, 方便阅读, 放在此处
// 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// ... 省略不重要的代码
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// ... 省略不重要的代码
return 1;
}
}
}

// AQS 继承过来的方法, 方便阅读, 放在此处
private void doAcquireShared(int arg) {
// 将当前线程关联到一个 Node 对象上, 模式为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再一次尝试获取读锁
int r = tryAcquireShared(arg);
// 成功
if (r >= 0) {
// ㈠
// r 表示可用资源数, 在这里总是 1 允许传播
//(唤醒 AQS 中下一个 Share 节点)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (
// 是否在获取读锁失败时阻塞(前一个阶段 waitStatus == Node.SIGNAL)
shouldParkAfterFailedAcquire(p, node) &&
// park 当前线程
parkAndCheckInterrupt()
) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}

// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置自己为 head
setHead(node);

// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
if (s == null || s.isShared()) {
// 进入 ㈡
doReleaseShared();
}
}
}

// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE, 为了解决 bug, 见后面分析
for (;;) {
Node h = head;
// 队列还有节点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 下一个节点 unpark 如果成功获取读锁
// 并且下下个节点还是 shared, 继续 doReleaseShared
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
读锁释放流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
static final class NonfairSync extends Sync {

// ReadLock 方法, 方便阅读, 放在此处
public void unlock() {
sync.releaseShared(1);
}

// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryReleaseShared(int unused) {
// ... 省略不重要的代码
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) {
// 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程
// 计数为 0 才是真正释放
return nextc == 0;
}
}
}

// AQS 继承过来的方法, 方便阅读, 放在此处
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果有其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性,见后文信号量 bug 分析
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}

读写锁

Semaphore原理

加锁解锁流程

Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一

刚开始,permits(state)为 3,这时 5 个线程来获取资源

image-20230113025722197

假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞

这时 Thread-4 释放了 permits,状态如下

image-20230113025944870

接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态

源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
// permits 即 state
super(permits);
}

// Semaphore 方法, 方便阅读, 放在此处
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}

// 尝试获得共享锁
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}

// Sync 继承过来的方法, 方便阅读, 放在此处
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (
// 如果许可已经用完, 返回负数, 表示获取失败, 进入 doAcquireSharedInterruptibly
remaining < 0 ||
// 如果 cas 重试成功, 返回正数, 表示获取成功
compareAndSetState(available, remaining)
) {
return remaining;
}
}
}

// AQS 继承过来的方法, 方便阅读, 放在此处
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再次尝试获取许可
int r = tryAcquireShared(arg);
if (r >= 0) {
// 成功后本线程出队(AQS), 所在 Node设置为 head
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
// r 表示可用资源数, 为 0 则不会继续传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

// Semaphore 方法, 方便阅读, 放在此处
public void release() {
sync.releaseShared(1);
}

// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
}

PROPAGATE的作用

早期存在 bug 的原因

  • releaseShared 方法
1
2
3
4
5
6
7
8
9
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
  • doAcquireShared 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 这里会有空档
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
  • setHeadAndPropagate 方法
1
2
3
4
5
6
7
8
9
10
private void setHeadAndPropagate(Node node, int propagate) {
setHead(node);
// 有空闲资源
if (propagate > 0 && node.waitStatus != 0) {
Node s = node.next;
// 下一个
if (s == null || s.isShared())
unparkSuccessor(node);
}
}
  • 假设存在某次循环中队列里排队的结点情况为 head(-1)->t1(-1)->t2(-1)
  • 假设存在将要信号量释放的 T3 和 T4,释放顺序为先 T3 后 T4

正常流程

image-20230113030331990

产生 bug 的情况

image-20230113030408856

修复前版本执行流程

  1. T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head 的等待状态从 -1 变为 0
  2. T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,假设返回值为 0(获取锁成功,但没有剩余资源量)
  3. T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个head),不满足条件,因此不调用 unparkSuccessor(head)
  4. T1 获取信号量成功,调用 setHeadAndPropagate 时,因为不满足 propagate > 0(2 的返回值也就是 propagate(剩余资源量) == 0),从而不会唤醒后继结点, T2 线程得不到唤醒

bug 修复后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置自己为 head
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
if (s == null || s.isShared()) {
doReleaseShared();
}
}
}
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
image-20230113030536646
  1. T3 调用 releaseShared(),直接调用了 unparkSuccessor(head),head 的等待状态从 -1 变为 0
  2. T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,假设返回值为 0(获取锁成功,但没有剩余资源量)
  3. T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为 PROPAGATE(-3)
  4. T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2

ConcurrentHashMap原理

JDK 7 HashMap 并发死链

测试代码

注意

  • 要在 JDK 7 下运行,否则扩容机制和 hash 的计算方法都变了
  • 以下测试代码是精心准备的,不要随便改动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public static void main(String[] args) {
// 测试 java 7 中哪些数字的 hash 结果相等
System.out.println("长度为16时,桶下标为1的key");
for (int i = 0; i < 64; i++) {
if (hash(i) % 16 == 1) {
System.out.println(i);
}
}
System.out.println("长度为32时,桶下标为1的key");
for (int i = 0; i < 64; i++) {
if (hash(i) % 32 == 1) {
System.out.println(i);
}
}
// 1, 35, 16, 50 当大小为16时,它们在一个桶内
final HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
// 放 12 个元素
map.put(2, null);
map.put(3, null);
map.put(4, null);
map.put(5, null);
map.put(6, null);
map.put(7, null);
map.put(8, null);
map.put(9, null);
map.put(10, null);
map.put(16, null);
map.put(35, null);
map.put(1, null);
System.out.println("扩容前大小[main]:"+map.size());
new Thread() {
@Override
public void run() {
// 放第 13 个元素, 发生扩容
map.put(50, null);
System.out.println("扩容后大小[Thread-0]:"+map.size());
}
}.start();
new Thread() {
@Override
public void run() {
// 放第 13 个元素, 发生扩容
map.put(50, null);
System.out.println("扩容后大小[Thread-1]:"+map.size());
}
}.start();
}
final static int hash(Object k) {
int h = 0;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
死链复现

调试工具使用 idea 在 HashMap 源码 590 行加断点

1
int newCapacity = newTable.length;

断点的条件如下,目的是让 HashMap 在扩容为 32 时,并且线程为 Thread-0 或 Thread-1 时停下来

1
2
3
4
5
newTable.length==32 &&
(
Thread.currentThread().getName().equals("Thread-0")||
Thread.currentThread().getName().equals("Thread-1")
)

断点暂停方式选择 Thread,否则在调试 Thread-0 时,Thread-1 无法恢复运行

运行代码,程序在预料的断点位置停了下来,输出

1
2
3
4
5
6
7
8
9
长度为16时,桶下标为1的key
1
16
35
50
长度为32时,桶下标为1的key
1
35
扩容前大小[main]:12

接下来进入扩容流程调试

在 HashMap 源码 594 行加断点

1
2
3
Entry<K,V> next = e.next; // 593
if (rehash) // 594
// ...

这是为了观察 e 节点和 next 节点的状态,Thread-0 单步执行到 594 行,再 594 处再添加一个断点(条件 Thread.currentThread().getName().equals(“Thread-0”))

这时可以在 Variables 面板观察到 e 和 next 变量,使用 view as -> Object 查看节点状态

1
2
e 		(1)->(35)->(16)->null
next (35)->(16)->null

在 Threads 面板选中 Thread-1 恢复运行,可以看到控制台输出新的内容如下,Thread-1 扩容已完成

1
newTable[1] 	(35)->(1)->null 
1
扩容后大小:13 

这时 Thread-0 还停在 594 处, Variables 面板变量的状态已经变化为

1
2
e 		(1)->null
next (35)->(1)->null

为什么呢,因为 Thread-1 扩容时链表也是后加入的元素放入链表头,因此链表就倒过来了,但 Thread-1 虽然结果正确,但它结束后 Thread-0 还要继续运行

接下来就可以单步调试(F8)观察死链的产生了

下一轮循环到 594,将 e 搬迁到 newTable 链表头

1
2
3
newTable[1] 	(1)->null
e (35)->(1)->null
next (1)->null

下一轮循环到 594,将 e 搬迁到 newTable 链表头

1
2
3
newTable[1] 	(35)->(1)->null
e (1)->null
next null

再看看源码

1
2
3
4
5
6
7
e.next = newTable[1];
// 这时 e (1,35)
// 而 newTable[1] (35,1)->(1,35) 因为是同一个对象
newTable[1] = e;
// 再尝试将 e 作为链表头, 死链已成
e = next;
// 虽然 next 是 null, 会进入下一个链表的复制, 但死链已经形成了
源码分析

HashMap 的并发死链发生在扩容时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 将 table 迁移至 newTable
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
// 1 处
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 2 处
// 将新元素加入 newTable[i], 原 newTable[i] 作为新元素的 next
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

假设 map 中初始元素是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
原始链表,格式:[下标] (key,next)
[1] (1,35)->(35,16)->(16,null)

线程 a 执行到 1 处 ,此时局部变量 e 为 (1,35),而局部变量 next 为 (35,16) 线程 a 挂起

线程 b 开始执行
第一次循环
[1] (1,null)

第二次循环
[1] (35,1)->(1,null)

第三次循环
[1] (35,1)->(1,null)
[17] (16,null)

切换回线程 a,此时局部变量 e 和 next 被恢复,引用没变但内容变了:e 的内容被改为 (1,null),而 next 的内
容被改为 (35,1) 并链向 (1,null)

第一次循环
[1] (1,null)

第二次循环,注意这时 e 是 (35,1) 并链向 (1,null) 所以 next 又是 (1,null)
[1] (35,1)->(1,null)

第三次循环,e 是 (1,null),而 next 是 null,但 e 被放入链表头,这样 e.next 变成了 352 处)
[1] (1,35)->(35,1)->(1,35)

已经是死链了

总结

  • 究其原因,是因为在多线程环境下使用了非线程安全的 map 集合
  • JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)

JDK 8 ConcurrentHashMap

重要属性和内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;

// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}

// hash 表
transient volatile Node<K,V>[] table;

// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;

// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}

// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}

// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}

// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}
重要方法
1
2
3
4
5
6
7
8
// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)

// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)

// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
构造器分析

可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建

1
2
3
4
5
6
7
8
9
10
11
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ...
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
Get 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// spread 方法能确保返回结果是正数
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果头结点已经是要查找的 key
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 正常遍历链表, 用 equals 比较
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
Put 流程

以下数组简称(table),链表简称(bin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 其中 spread 方法会综合高位低位, 具有更好的 hash 性
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f 是链表头节点
// fh 是链表头结点的 hash
// i 是链表在 table 中的下标
Node<K,V> f; int n, i, fh;
// 要创建 table
if (tab == null || (n = tab.length) == 0)
// 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环
tab = initTable();
// 要创建链表头节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 添加链表头使用了 cas, 无需 synchronized
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 帮忙扩容
else if ((fh = f.hash) == MOVED)
// 帮忙之后, 进入下一轮循环
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 锁住链表头节点
synchronized (f) {
// 再次确认链表头节点没有被移动
if (tabAt(tab, i) == f) {
// 链表
if (fh >= 0) {
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到相同的 key
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// 更新
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 已经是最后的节点了, 新增 Node, 追加至链表尾
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNode
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
// 释放链表头节点的锁
}

if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加 size 计数
addCount(1L, binCount);
return null;
}
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield();
// 尝试将 sizeCtl 设置为 -1(表示初始化 table)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
// check 是之前 binCount 的个数
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if (
// 已经有了 counterCells, 向 cell 累加
(as = counterCells) != null ||
// 还没有, 向 baseCount 累加
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (
// 还没有 counterCells
as == null || (m = as.length - 1) < 0 ||
// 还没有 cell
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// cell cas 增加计数失败
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
) {
// 创建累加单元数组和cell, 累加重试
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
// 获取元素个数
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// newtable 已经创建了,帮忙扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 需要扩容,这时 newtable 未创建
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
Size 计算流程

size 计算实际发生在 put,remove 改变集合元素的操作之中

  • 没有竞争发生,向 baseCount 累加计数
  • 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
    • counterCells 初始有两个 cell
    • 如果计数竞争比较激烈,会创建新的 cell 来累加计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
// 将 baseCount 计数与所有 cell 计数累加
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}

Java 8 –> 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)

  • 初始化,使用 cas 来保证并发安全,懒惰初始化 table
  • 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头
  • put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部
  • get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新 table 进行搜索
  • 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中
  • size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加即可

参考资料

源码分析

其它实现 Cliff Click’s high scale lib

ConcurrentHashMap

LinkedBlockingQueue原理

入队出队

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {

E item;

/**
* 下列三种情况之一
* - 真正的后继节点
* - 自己, 发生在出队时
* - null, 表示是没有后继节点, 是最后了
*/

Node<E> next;

Node(E x) { item = x; }
}
}

初始化链表 last = head = new Node(null); Dummy 节点用来占位,item 为 null

image-20230113143859707

当一个节点入队 last = last.next = node;

image-20230113143925234

再来一个节点入队 last = last.next = node;

image-20230113143957098

出队

1
2
3
4
5
6
7
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;

h = head

image-20230113144043828

first = h.next

image-20230113144112385

h.next = h

image-20230113144138230

head = first

image-20230113144210147
1
2
3
E x = first.item;
first.item = null;
return x;
image-20230113144235034

加锁分析

==高明之处==在于用了两把锁和 dummy 节点

  • 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
  • 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
    • 消费者与消费者线程仍然串行
    • 生产者与生产者线程仍然串行

线程安全分析

  • 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争
  • 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
  • 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
1
2
3
4
5
// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();

// 用户 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();

put 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
// count 用来维护元素计数
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 满了等待
while (count.get() == capacity) {
// 倒过来读就好: 等待 notFull
notFull.await();
}
// 有空位, 入队且计数加一
enqueue(node);
c = count.getAndIncrement();
// 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列中有一个元素, 叫醒 take 线程
if (c == 0)
// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
signalNotEmpty();
}

take 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
signalNotFull()
return x;
}

由 put 唤醒 put 是为了避免信号不足

性能比较

主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较

  • Linked 支持有界,Array 强制有界
  • Linked 实现是链表,Array 实现是数组
  • Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
  • Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
  • Linked 两把锁,Array 一把锁

BlockingQueue

ConcurrentLinkedQueue原理

模仿 ConcurrentLinkedQueue

初始代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package cn.itcast.concurrent.thirdpart.test;
import java.util.Collection;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicReference;
public class Test3 {
public static void main(String[] args) {
MyQueue<String> queue = new MyQueue<>();
queue.offer("1");
queue.offer("2");
queue.offer("3");
System.out.println(queue);
}
}
class MyQueue<E> implements Queue<E> {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Node<E> p = head; p != null; p = p.next.get()) {
E item = p.item;
if (item != null) {
sb.append(item).append("->");
}
}
sb.append("null");
return sb.toString();
}
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean contains(Object o) {
return false;
}
@Override
public Iterator<E> iterator() {
return null;
}
@Override
public Object[] toArray() {
return new Object[0];
}
@Override
public <T> T[] toArray(T[] a) {
return null;
}
@Override
public boolean add(E e) {
return false;
}
@Override
public boolean remove(Object o) {
return false;
}
@Override
public boolean containsAll(Collection<?> c) {
return false;
}
@Override
public boolean addAll(Collection<? extends E> c) {
return false;
}
@Override
public boolean removeAll(Collection<?> c) {
return false;
}
@Override
public boolean retainAll(Collection<?> c) {
return false;
}
@Override
public void clear() {
}
@Override
public E remove() {
return null;
}
@Override
public E element() {
return null;
}
@Override
public E peek() {
return null;
}
public MyQueue() {
head = last = new Node<>(null, null);
}
private volatile Node<E> last;
private volatile Node<E> head;
private E dequeue() {
/*Node<E> h = head;
Node<E> first = h.next;
h.next = h;
head = first;
E x = first.item;
first.item = null;
return x;*/
return null;
}
@Override
public E poll() {
return null;
}
@Override
public boolean offer(E e) {
return true;
}
static class Node<E> {
volatile E item;
public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<>(next);
}
AtomicReference<Node<E>> next;
}
}

offer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean offer(E e) {
Node<E> n = new Node<>(e, null);
while(true) {
// 获取尾节点
AtomicReference<Node<E>> next = last.next;
// S1: 真正尾节点的 next 是 null, cas 从 null 到新节点
if(next.compareAndSet(null, n)) {
// 这时的 last 已经是倒数第二, next 不为空了, 其它线程的 cas 肯定失败
// S2: 更新 last 为倒数第一的节点
last = n;
return true;
}
}
}

ConcurrentLinkedQueue

模式

同步-保护性暂停模式

基本概念

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果

核心

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

基础实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get() {
synchronized (lock) {
// 条件不满足则等待
while (response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
lock.notifyAll();
}
}
}

基础应用

一个线程等待另一个线程的执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
try {
// 子线程执行下载
List<String> response = download();
log.debug("download complete...");
guardedObject.complete(response);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
log.debug("waiting...");
// 主线程阻塞等待
Object response = guardedObject.get();
log.debug("get response: [{}] lines", ((List<String>) response).size());
}

执行结果

1
2
3
08:42:18.568 [main] c.TestGuardedObject - waiting...
08:42:23.312 [Thread-0] c.TestGuardedObject - download complete...
08:42:23.312 [main] c.TestGuardedObject - get response: [3] lines

GuardedObject 带超时

如果要控制超时时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class GuardedObjectV2 {
private Object response;
private final Object lock = new Object();
public Object get(long millis) {
synchronized (lock) {
// 1) 记录最初时间
long begin = System.currentTimeMillis();
// 2) 已经经历的时间
long timePassed = 0;
while (response == null) {
// 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等
long waitTime = millis - timePassed;
log.debug("waitTime: {}", waitTime);
if (waitTime <= 0) {
log.debug("break...");
break;
}
try {
lock.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3) 如果提前被唤醒,这时已经经历的时间假设为 400
timePassed = System.currentTimeMillis() - begin;
log.debug("timePassed: {}, object is null {}",
timePassed, response == null);
}
return response;
}
}
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
log.debug("notify...");
lock.notifyAll();
}
}
}

测试 –没有超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
GuardedObjectV2 v2 = new GuardedObjectV2();
new Thread(() -> {
sleep(1);
v2.complete(null);
sleep(1);
v2.complete(Arrays.asList("a", "b", "c"));
}).start();
Object response = v2.get(2500);
if (response != null) {
log.debug("get response: [{}] lines", ((List<String>) response).size());
} else {
log.debug("can't get response");
}
}

输出

1
2
3
4
5
6
7
08:49:39.917 [main] c.GuardedObjectV2 - waitTime: 2500
08:49:40.917 [Thread-0] c.GuardedObjectV2 - notify...
08:49:40.917 [main] c.GuardedObjectV2 - timePassed: 1003, object is null true
08:49:40.917 [main] c.GuardedObjectV2 - waitTime: 1497
08:49:41.918 [Thread-0] c.GuardedObjectV2 - notify...
08:49:41.918 [main] c.GuardedObjectV2 - timePassed: 2004, object is null false
08:49:41.918 [main] c.TestGuardedObjectV2 - get response: [3] lines

测试 –超时

1
2
// 等待时间不足
List<String> lines = v2.get(1500);

输出

1
2
3
4
5
6
7
8
9
08:47:54.963 [main] c.GuardedObjectV2 - waitTime: 1500
08:47:55.963 [Thread-0] c.GuardedObjectV2 - notify...
08:47:55.963 [main] c.GuardedObjectV2 - timePassed: 1002, object is null true
08:47:55.963 [main] c.GuardedObjectV2 - waitTime: 498
08:47:56.461 [main] c.GuardedObjectV2 - timePassed: 1500, object is null true
08:47:56.461 [main] c.GuardedObjectV2 - waitTime: 0
08:47:56.461 [main] c.GuardedObjectV2 - break...
08:47:56.461 [main] c.TestGuardedObjectV2 - can't get response
08:47:56.963 [Thread-0] c.GuardedObjectV2 - notify...

原理见 join 原理

GuardedObject 多任务

图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员

如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理

image-20230113231659219

新增 id 用来标识 Guarded Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class GuardedObject {
// 标识 Guarded Object
private int id;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// timeout 表示要等待多久 2000
public Object get(long timeout) {
synchronized (this) {
// 开始时间 15:00:00
long begin = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
while (response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
// 经历的时间超过了最大等待时间时,退出循环
if (timeout - passedTime <= 0) {
break;
}
try {
this.wait(waitTime); // 虚假唤醒 15:00:01
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}

中间解耦类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Mailboxes {
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
// 产生唯一 id
private static synchronized int generateId() {
return id++;
}
public static GuardedObject getGuardedObject(int id) {
return boxes.remove(id);
}
public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}

业务相关类

1
2
3
4
5
6
7
8
9
10
class People extends Thread{
@Override
public void run() {
// 收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信 id:{}", guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), mail);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Postman extends Thread {
private int id;
private String mail;
public Postman(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
log.debug("送信 id:{}, 内容:{}", id, mail);
guardedObject.complete(mail);
}
}

测试

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Sleeper.sleep(1);
for (Integer id : Mailboxes.getIds()) {
new Postman(id, "内容" + id).start();
}
}

运行结果

1
2
3
4
5
6
7
8
9
10:35:05.689 c.People [Thread-1] - 开始收信 id:3
10:35:05.689 c.People [Thread-2] - 开始收信 id:1
10:35:05.689 c.People [Thread-0] - 开始收信 id:2
10:35:06.688 c.Postman [Thread-4] - 送信 id:2, 内容:内容2
10:35:06.688 c.Postman [Thread-5] - 送信 id:1, 内容:内容1
10:35:06.688 c.People [Thread-0] - 收到信 id:2, 内容:内容2
10:35:06.688 c.People [Thread-2] - 收到信 id:1, 内容:内容1
10:35:06.688 c.Postman [Thread-3] - 送信 id:3, 内容:内容3
10:35:06.689 c.People [Thread-1] - 收到信 id:3, 内容:内容3

同步-Balking

基本概念

Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

基础实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MonitorService {
// 用来表示是否已经有线程已经在执行启动了
private volatile boolean starting;
public void start() {
log.info("尝试启动监控线程...");
synchronized (this) {
if (starting) {
return;
}
starting = true;
}

// 真正启动监控线程...
}
}

当前端页面多次点击按钮调用 start 时

输出

1
2
3
4
5
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(false)
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 监控线程已启动...
[http-nio-8080-exec-2] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
[http-nio-8080-exec-3] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
[http-nio-8080-exec-4] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)

它还经常用来实现线程安全的单例

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static synchronized Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}

INSTANCE = new Singleton();
return INSTANCE;
}
}

对比一下保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待。

同步-顺序控制

固定运行顺序

比如,必须先 2 后 1 打印

基于 wait-notify
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 用来同步的对象
static Object obj = new Object();
// t2 运行标记, 代表 t2 是否执行过
static boolean t2runed = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
// 如果 t2 没有执行过
while (!t2runed) {
try {
// t1 先等一会
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(1);
});
Thread t2 = new Thread(() -> {
System.out.println(2);
synchronized (obj) {
// 修改运行标记
t2runed = true;
// 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
obj.notifyAll();
}
});
t1.start();
t2.start();
}
基于 park-unpark

可以看到,实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个

可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread t1 = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) { }
// 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
LockSupport.park();
System.out.println("1");
});
Thread t2 = new Thread(() -> {
System.out.println("2");
// 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
LockSupport.unpark(t1);
});
t1.start();
t2.start();

park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』

交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现

基于 wait-notify
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SyncWaitNotify {
private int flag;
private int loopNumber;
public SyncWaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
public void print(int waitFlag, int nextFlag, String str) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while (this.flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {
syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {
syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {
syncWaitNotify.print(3, 1, "c");
}).start();
基于 lock条件变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class AwaitSignal extends ReentrantLock {
public void start(Condition first) {
this.lock();
try {
log.debug("start");
first.signal();
} finally {
this.unlock();
}
}
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
current.await();
log.debug(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
// 循环次数
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
AwaitSignal as = new AwaitSignal(5);
Condition aWaitSet = as.newCondition();
Condition bWaitSet = as.newCondition();
Condition cWaitSet = as.newCondition();
new Thread(() -> {
as.print("a", aWaitSet, bWaitSet);
}).start();
new Thread(() -> {
as.print("b", bWaitSet, cWaitSet);
}).start();
new Thread(() -> {
as.print("c", cWaitSet, aWaitSet);
}).start();
as.start(aWaitSet);

注意

该实现没有考虑 a,b,c 线程都就绪再开始

基于 park-unpark
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class SyncPark {
private int loopNumber;
private Thread[] threads;
public SyncPark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void setThreads(Thread... threads) {
this.threads = threads;
}
public void print(String str) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(nextThread());
}
}
private Thread nextThread() {
Thread current = Thread.currentThread();
int index = 0;
for (int i = 0; i < threads.length; i++) {
if(threads[i] == current) {
index = i;
break;
}
}
if(index < threads.length - 1) {
return threads[index+1];
} else {
return threads[0];
}
}
public void start() {
for (Thread thread : threads) {
thread.start();
}
LockSupport.unpark(threads[0]);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {
syncPark.print("a");
});
Thread t2 = new Thread(() -> {
syncPark.print("b");
});
Thread t3 = new Thread(() -> {
syncPark.print("c\n");
});
syncPark.setThreads(t1, t2, t3);
syncPark.start();

异步-生产者/消费者

基本概念

核心

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式
image-20230113232536862

基础实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Message {
private int id;
private Object message;
public Message(int id, Object message) {
this.id = id;
this.message = message;
}
public int getId() {
return id;
}
public Object getMessage() {
return message;
}
}

class MessageQueue {
private LinkedList<Message> queue;
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
queue = new LinkedList<>();
}
public Message take() {
synchronized (queue) {
while (queue.isEmpty()) {
log.debug("没货了, wait");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message message = queue.removeFirst();
queue.notifyAll();
return message;
}
}
public void put(Message message) {
synchronized (queue) {
while (queue.size() == capacity) {
log.debug("库存已达上限, wait");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(message);
queue.notifyAll();
}
}
}

基础应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MessageQueue messageQueue = new MessageQueue(2);
// 4 个生产者线程, 下载任务
for (int i = 0; i < 4; i++) {
int id = i;
new Thread(() -> {
try {
log.debug("download...");
List<String> response = Downloader.download();
log.debug("try put message({})", id);
messageQueue.put(new Message(id, response));
} catch (IOException e) {
e.printStackTrace();
}
}, "生产者" + i).start();
}
// 1 个消费者线程, 处理结果
new Thread(() -> {
while (true) {
Message message = messageQueue.take();
List<String> response = (List<String>) message.getMessage();
log.debug("take message({}): [{}] lines", message.getId(), response.size());
}
}, "消费者").start();

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
10:48:38.070 [生产者3] c.TestProducerConsumer - download...
10:48:38.070 [生产者0] c.TestProducerConsumer - download...
10:48:38.070 [消费者] c.MessageQueue - 没货了, wait
10:48:38.070 [生产者1] c.TestProducerConsumer - download...
10:48:38.070 [生产者2] c.TestProducerConsumer - download...
10:48:41.236 [生产者1] c.TestProducerConsumer - try put message(1)
10:48:41.237 [生产者2] c.TestProducerConsumer - try put message(2)
10:48:41.236 [生产者0] c.TestProducerConsumer - try put message(0)
10:48:41.237 [生产者3] c.TestProducerConsumer - try put message(3)
10:48:41.239 [生产者2] c.MessageQueue - 库存已达上限, wait
10:48:41.240 [生产者1] c.MessageQueue - 库存已达上限, wait
10:48:41.240 [消费者] c.TestProducerConsumer - take message(0): [3] lines
10:48:41.240 [生产者2] c.MessageQueue - 库存已达上限, wait
10:48:41.240 [消费者] c.TestProducerConsumer - take message(3): [3] lines
10:48:41.240 [消费者] c.TestProducerConsumer - take message(1): [3] lines
10:48:41.240 [消费者] c.TestProducerConsumer - take message(2): [3] lines
10:48:41.240 [消费者] c.MessageQueue - 没货了, wait

异步-工作线程

基本概念

让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式

例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)

注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率

例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工

饥饿

固定大小线程池会有饥饿现象

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
    • 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
    • 后厨做菜:没啥说的,做就是了
  • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TestDeadLock {
static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
static Random RANDOM = new Random();
static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> {
log.debug("处理点餐...");
Future<String> f = executorService.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
/*executorService.execute(() -> {
log.debug("处理点餐...");
Future<String> f = executorService.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});*/
}
}

输出

1
2
3
17:21:27.883 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
17:21:27.891 c.TestDeadLock [pool-1-thread-2] - 做菜
17:21:27.891 c.TestDeadLock [pool-1-thread-1] - 上菜: 烤鸡翅

当注释取消后,可能的输出

1
2
17:08:41.339 c.TestDeadLock [pool-1-thread-2] - 处理点餐...
17:08:41.339 c.TestDeadLock [pool-1-thread-1] - 处理点餐...

解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程池,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TestDeadLock {
static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
static Random RANDOM = new Random();
static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService waiterPool = Executors.newFixedThreadPool(1);
ExecutorService cookPool = Executors.newFixedThreadPool(1);
waiterPool.execute(() -> {
log.debug("处理点餐...");
Future<String> f = cookPool.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
waiterPool.execute(() -> {
log.debug("处理点餐...");
Future<String> f = cookPool.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
}

输出

1
2
3
4
5
6
17:25:14.626 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
17:25:14.630 c.TestDeadLock [pool-2-thread-1] - 做菜
17:25:14.631 c.TestDeadLock [pool-1-thread-1] - 上菜: 地三鲜
17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
17:25:14.632 c.TestDeadLock [pool-2-thread-1] - 做菜
17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 上菜: 辣子鸡丁

线程池权衡

创建多少线程池合适

  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存
CPU 密集型运算

通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

I/O 密集型运算

CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。
$$
线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间
$$
例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 50% = 8

例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 10% = 40

自定义线程池

image-20230114002023472

步骤1:自定义拒绝策略接口

1
2
3
4
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}

步骤2:自定义任务队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class BlockingQueue<T> {
// 1. 任务队列
private Deque<T> queue = new ArrayDeque<>();

// 2. 锁
private ReentrantLock lock = new ReentrantLock();

// 3. 生产者条件变量
private Condition fullWaitSet = lock.newCondition();

// 4. 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();

// 5. 容量
private int capcity;
public BlockingQueue(int capcity) {
this.capcity = capcity;
}

// 带超时阻塞获取
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
// 将 timeout 统一转换为 纳秒
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
// 返回值是剩余时间
if (nanos <= 0) {
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}

// 阻塞获取
public T take() {
lock.lock();
try {
while (queue.isEmpty()) {
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}

// 阻塞添加
public void put(T task) {
lock.lock();
try {
while (queue.size() == capcity) {
try {
log.debug("等待加入任务队列 {} ...", task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}

// 带超时时间阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capcity) {
try {
if(nanos <= 0) {
return false;
}
log.debug("等待加入任务队列 {} ...", task);
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
// 判断队列是否满
if(queue.size() == capcity) {
rejectPolicy.reject(this, task);
} else { // 有空闲
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
}
} finally {
lock.unlock();
}
}
}

步骤3:自定义线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务时的超时时间
private long timeout;
private TimeUnit timeUnit;
private RejectPolicy<Runnable> rejectPolicy;
// 执行任务
public void execute(Runnable task) {
// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
// 如果任务数超过 coreSize 时,加入任务队列暂存
synchronized (workers) {
if(workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("新增 worker{}, {}", worker, task);
workers.add(worker);
worker.start();
} else {
// taskQueue.put(task);
// 1) 死等
// 2) 带超时等待
// 3) 让调用者放弃任务执行
// 4) 让调用者抛出异常
// 5) 让调用者自己执行任务
taskQueue.tryPut(rejectPolicy, task);
}
}
}
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,
RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 执行任务
// 1) 当 task 不为空,执行任务
// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
// while(task != null || (task = taskQueue.take()) != null) {
while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
log.debug("正在执行...{}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
log.debug("worker 被移除{}", this);
workers.remove(this);
}
}
}
}

步骤4:测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1,
1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
// 1. 死等
// queue.put(task);
// 2) 带超时等待
// queue.offer(task, 1500, TimeUnit.MILLISECONDS);
// 3) 让调用者放弃任务执行
// log.debug("放弃{}", task);
// 4) 让调用者抛出异常
// throw new RuntimeException("任务执行失败 " + task);
// 5) 让调用者自己执行任务
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}

终止-两阶段终止模式

Two Phase Termination

在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

错误思路

  • 使用线程对象的 stop() 方法停止线程
    • stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程
    • 目的仅是停止一个线程,但这种做法会让整个程序都停止

两阶段终止模式

image-20230114002437515
基于 isInterrupted

interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class TPTInterrupt {
private Thread thread;
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
current.interrupt();
}
// 执行监控操作
}
},"监控线程");
thread.start();
}
public void stop() {
thread.interrupt();
}
}

调用

1
2
3
4
5
6
TPTInterrupt t = new TPTInterrupt();
t.start();

Thread.sleep(3500);
log.debug("stop");
t.stop();

结果

1
2
3
4
5
11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事
基于 停止标记
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
private Thread thread;
private volatile boolean stop = false;

public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
}
// 执行监控操作
}
},"监控线程");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}

调用

1
2
3
4
5
6
TPTVolatile t = new TPTVolatile();
t.start();

Thread.sleep(3500);
log.debug("stop");
t.stop();

结果

1
2
3
4
5
11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事

案例:JVM 内存监控

线程安全单例

单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用 getInstance)时的线程安全,并思考注释中的问题

饿汉式:类加载就会导致该单实例对象被创建

懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

饿汉单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
private Singleton() {}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?
private static final Singleton INSTANCE = new Singleton();
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}

枚举单例

1
2
3
4
5
6
7
8
9
// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
INSTANCE;
}

懒汉单例

1
2
3
4
5
6
7
8
9
10
11
12
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}

DCL 懒汉单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?
private static volatile Singleton INSTANCE = null;

// 问题2:对比实现3, 说出这样做的意义
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}

静态内部类懒汉单例

1
2
3
4
5
6
7
8
9
10
11
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}

享元模式

基本概念

定义 英文名称:Flyweight pattern. 当需要重用数量有限的同一类对象时

wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects

出自 “Gang of Four” design patterns

归类 Structual patterns

应用体现

包装类

在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象:

1
2
3
4
5
6
7
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}

注意

  • Byte, Short, Long 缓存的范围都是 -128~127
  • Character 缓存的范围是 0~127
  • Integer的默认范围是 -128~127
    • 最小值不能变
    • 但最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变
  • Boolean 缓存了 TRUE 和 FALSE
String 串池
BigDecimal BigInteger

DIY

例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Pool {
// 1. 连接池大小
private final int poolSize;

// 2. 连接对象数组
private Connection[] connections;

// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;

// 4. 构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i+1));
}
}

// 5. 借连接
public Connection borrow() {
while(true) {
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if(states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 如果没有空闲连接,当前线程进入等待
synchronized (this) {
try {
log.debug("wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
synchronized (this) {
log.debug("free {}", conn);
this.notifyAll();
}
break;
}
}
}
}

class MockConnection implements Connection {
// 实现略
}

使用连接池:

1
2
3
4
5
6
7
8
9
10
11
12
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection conn = pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(conn);
}).start();
}

以上实现没有考虑:

  • 连接的动态增长与收缩
  • 连接保活(可用性检测)
  • 等待超时处理
  • 分布式 hash

对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用 apache commons pool,例如redis连接池可以参考jedis中关于连接池的实现

MyBatisPlus_Note

1. MyBatisPlus概述

MyBatisPlus主要用于更简单的解决 CRUD –自动化完成 CRUD

同类型的还有 JPA tk-mapper MyBatisPlus –课后简单了解

MyBatisPlus 简介

MyBatis 的诞生是为了 简化 JDBC

而 MyBatisPlus 的诞生 是为了 简化 MyBatis

所以 人类的本质 是偷懒

官网:https://baomidou.com/

MyBatisPlus 特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作 使用 BaseMapper<T> 极简了常用 CRUD
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求 简单的 CRUD 将不再需要自己编写
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用 (自动生成代码 –类似于 EasyCode)
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

MyBatisPlus 支持数据库:

任何能使用 MyBatis 进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下,如果不在下列表查看分页部分教程 PR 您的支持。

  • MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb
  • 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库

2. 快速入门

通过官网 MyBatisPlus 的文档自己研究 特性 锻炼学习能力

快速开始网址:https://baomidou.com/pages/226c21/#%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B7%A5%E7%A8%8B

使用步骤概述:

  1. 导入对应依赖
  2. 研究依赖如何配置
  3. 代码如何编写
  4. 提高扩展技术能力

具体实现步骤

  1. 创建数据库 mybatis_plus 创建 user 表 并插入 默认数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DROP TABLE IF EXISTS user;

CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);

DELETE FROM user;

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

-- 真实开发中 通常必须包含 version(乐观锁) deleted(逻辑删除) gmt_create gmt_modified 字段

==注意 在真实开发中 通常必须包含 version(乐观锁) deleted(逻辑删除) gmt_create gmt_modified 字段==

  1. 编写项目 初始化项目 SpringBoot 项目
  2. 导入相关依赖
    • 数据库驱动
    • lombok
    • mybatis-plus
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>

<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>

<!-- mybatis-plus -->
<!-- mybatis-plus 是自己开发的 而并非官方的 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>

==注意 不要同时导入 mybatis 与 mybatis-plus 依赖==

  1. 连接数据库 与 mybatis 相同
1
2
3
4
5
6
7
# mysql 5 与 mysql 8 驱动不同 com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# mysql 8 驱动必须使用 com.mysql.cj.jdbc.Driver 同时需要增加时区配置 serverTimezone=GMT%2B8
  1. 对比学习 操作步骤
    • 使用 mybatis 操作步骤 创建 pojo -> dao ( 连接 mybatis 配置 mapper.xml 映射文件 ) -> service -> controller
    • 使用 mybatis-plus 操作步骤 创建 pojo -> mapper 接口 -> 直接使用简单 CRUD

参考代码

application.properties

1
2
3
4
5
6
7
# mysql 5 与 mysql 8 驱动不相同 com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# mysql 8 使用驱动 com.mysql.cj.jdbc.Driver 设置时区 serverTimezone=GMT%2B8

User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.mybatisplustest.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}

UserMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.mybatisplustest.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mybatisplustest.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

// 使用 mybatis-plus 不需要在提供 mapper.xml 映射文件 只需要在 UserMapper 接口上 实现 BaseMapper 接口即可
@Mapper // 注解表示为一个 Mapper 文件
//@Repository
public interface UserMapper extends BaseMapper<User> {
// 此时 所偶 CRUD 操作就已经编写完成
// 不需要再像 以前一样编写大量的配置文件
}

MyBatisPlusTestApplicationTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.mybatisplustest;

import com.example.mybatisplustest.mapper.UserMapper;
import com.example.mybatisplustest.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class MyBatisPlusTestApplicationTests {
// 继承了 BaseMapper 可以直接使用 简单的 CRUD 所有方法均来自 父接口
// 同样 我们也可以编写自己的扩展方法
@Autowired
private UserMapper userMapper;

@Test
void contextLoads() {
// 查询所有用户
// 参数为 Wrapper --条件构造器 不使用 可为 null
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}
}

==注意 必须要在 注启动类上配置 扫描 mapper 包下所有 mapper 接口==

思考:

  • SQL在哪里编写的
  • 方法在哪里实现的

3. 配置日志

配置日志 可以查看到 具体执行的 SQL 语句

当然在项目上线时 需要将 日志 功能全部关闭 因为配置日志会导致损耗性能

配置日志步骤

application.properties

1
2
# 配置 mybatis-plus 日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@518ddd3b] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1821228886 wrapping com.mysql.cj.jdbc.ConnectionImpl@493b01ef] will not be managed by Spring
==> Preparing: SELECT id,name,age,email FROM user
==> Parameters:
<== Columns: id, name, age, email
<== Row: 1, Jone, 18, test1@baomidou.com
<== Row: 2, Jack, 20, test2@baomidou.com
<== Row: 3, Tom, 28, test3@baomidou.com
<== Row: 4, Sandy, 21, test4@baomidou.com
<== Row: 5, Billie, 24, test5@baomidou.com
<== Total: 5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@518ddd3b]
User(id=1, name=Jone, age=18, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
User(id=3, name=Tom, age=28, email=test3@baomidou.com)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com)
User(id=5, name=Billie, age=24, email=test5@baomidou.com)

==注意 配置完日志后 就可以通过 自动生成的 SQL 学习 SQL 语句 同时 更好的理解 MyBatis-Plus==

4. 简单 CRUD 操作详解

Insert

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.example.mybatisplustest;

import com.baomidou.mybatisplus.extension.api.Assert;
import com.example.mybatisplustest.mapper.UserMapper;
import com.example.mybatisplustest.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class MyBatisPlusTestApplicationTests {

// 继承了 BaseMapper 可以直接使用 简单的 CRUD 所有方法均来自 父接口
// 同样 我们也可以编写自己的扩展方法
@Autowired
private UserMapper userMapper;

@Test
void contextLoads() {
// 查询所有用户
// 参数为 Wrapper --条件构造器 不使用 可为 null
List<User> userList = userMapper.selectList(null);
// Assert.isTrue();
userList.forEach(System.out::println);
}

// 测试插入
@Test
public void TestInsert() {
User user = new User();
user.setName("Line");
user.setAge(18);
user.setEmail("1031282691@qq.com");

// 上述可见没有插入 ID
/*
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@518ddd3b] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1720760826 wrapping com.mysql.cj.jdbc.ConnectionImpl@6c8dbf56] will not be managed by Spring
==> Preparing: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )

==> Parameters: 1580171623430950913(Long), Line(String), 18(Integer), 1031282691@qq.com(String)

<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@518ddd3b]
1
User(id=1580171623430950913, name=Line, age=18, email=1031282691@qq.com)
*/
// 但是从结果可知 mybatis-plus 会自动生成 ID

int ret = userMapper.insert(user);
System.out.println(ret); // 打印受影响的行数
System.out.println(user); // 自动生成的 ID 会自动回填到 user 对象的属性中
}
}

思考:

  • MyBatis-Plus 在什么环节 从哪里 自动生成了 ID
  • 数据库插入的 ID 默认值 为全局唯一 ID ( 全局默认 ID 可以通过一些策略来生成 )

查阅资料可知 其中主要使用 ==主键生成策略==

主键生成策略

使用雪花算法

参考文章:https://www.cnblogs.com/haoxinyue/p/5208136.html

雪花算法

Twitter的snowflake算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心 –分布在不同地区,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

具体实现的代码可以参看https://github.com/twitter/snowflake。雪花算法支持的TPS可以达到419万左右(2^22*1000)。

雪花算法在工程实现上有单机版本和分布式版本。单机版本如下,分布式版本可以参看美团leaf算法:https://github.com/Meituan-Dianping/Leaf

通过雪花算法 可以保证 此 ID 几乎全球唯一 ==–不人为指定相同的情况下==

TODO:探究 @TableId(type = IdType.ID_WORKER) 源码 以及 工作原理 –默认ID_WORKER 为全局唯一ID

主键策略

  • ID_WORKER –默认是ID_WORKER策略 -全局唯一 ID 策略
    使用 需要在实体类相关字段上 添加 @TableId(type = IdType.ID_WORKER)
  • AUTO –主键自增策略
    注意 使用主键自增策略 一定要 添加 @TableId(type = IdType.AUTO) ==并在 数据库 ID主键 上设置 自增属性 否则会报错==
  • NONE –未设置主键
  • INPUT –手动输入
  • UUID –全局唯一ID -UUID
  • ID_WORKER_STR –ID_WORKER的字符串表示 -一个是数字表示 一个是字符串表示

相关源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.baomidou.mybatisplus.annotation;

public enum IdType {
AUTO(0),
NONE(1),
INPUT(2),
ID_WORKER(3),
UUID(4),
ID_WORKER_STR(5);

private int key;

private IdType(int key) {
this.key = key;
}

public int getKey() {
return this.key;
}
}

参考代码

User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.mybatisplustest.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
// 对应 数据库 中的主键 (uuid 自增id 雪花算法 redis zookeeper)
@TableId(type = IdType.ID_WORKER)
// @TableId(type = IdType.AUTO)
// @TableId(type = IdType.NONE)
// @TableId(type = IdType.INPUT) // 一旦手动输入 ID 之后 就必须要自己配置 ID
// @TableId(type = IdType.UUID)
// @TableId(type = IdType.ID_WORKER_STR)
private Long id;
private String name;
private Integer age;
private String email;
}

Update

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testUpdate() {
User user = new User();
user.setId(1580171623430950913L);
user.setName("李宁");

/*
日志:
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2d85fb64] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@513241240 wrapping com.mysql.cj.jdbc.ConnectionImpl@630390b9] will not be managed by Spring
==> Preparing: UPDATE user SET name=? WHERE id=?
==> Parameters: 李宁(String), 1580171623430950913(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2d85fb64]
1
*/
// 注意 虽然 userMapper.updateById 调用的 updateById 方法 但是 其输入参数 为一个 T 泛型 即你创建的 实体类
// 根据多次 测试 可以很明显的 看出 mybatis-plus 可以根据 所给对象的参数 通过条件自动拼接动态 sql
int ret = userMapper.updateById(user);
System.out.println(ret);
}

==注意 所有 SQL 都是 mybatisplus通过内置的算法帮你 动态配置的 可以通过查看日志的 方式 来进行学习==

MyBatis-Plus 属性自动填充

根据 阿里巴巴 开发手册:所有数据库表 gmt_create gmt_modified 通常所有表上都要配置 且都需要自动化填充数据

由此 创建时间 修改实践 这系列操作 都应该通过自动化来完成 不希望通过开发人员手动更新

自动填充方式

  • 方式一 数据库级别 由于工作中 通常不会轻易更改数据库 工作中不推荐使用

    1. 在数据库 表中 添加 相对应的表字段 create_time update_time

      image-20221013201516276

    2. 同步实体类 并测试

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      @Test
      public void testUpdate() {
      User user = new User();
      user.setId(1580171623430950913L);
      user.setName("李宁");
      user.setAge(22);

      /*
      日志:
      Creating a new SqlSession
      SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2d85fb64] was not registered for synchronization because synchronization is not active
      JDBC Connection [HikariProxyConnection@513241240 wrapping com.mysql.cj.jdbc.ConnectionImpl@630390b9] will not be managed by Spring
      ==> Preparing: UPDATE user SET name=? WHERE id=?
      ==> Parameters: 李宁(String), 1580171623430950913(Long)
      <== Updates: 1
      Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2d85fb64]
      1
      */
      // 注意 虽然 userMapper.updateById 调用的 updateById 方法 但是 其输入参数 为一个 T 泛型 即你创建的 实体类
      // 根据多次 测试 可以很明显的 看出 mybatis-plus 可以根据 所给对象的参数 通过条件自动拼接动态 sql
      int ret = userMapper.updateById(user);
      System.out.println(ret);
      }
    3. 查看测试结果
      image-20221013201550898

参考代码

user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.mybatisplustest.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
// 对应 数据库 中的主键 (uuid 自增id 雪花算法 redis zookeeper)
@TableId(type = IdType.ID_WORKER)
// @TableId(type = IdType.AUTO)
// @TableId(type = IdType.NONE)
// @TableId(type = IdType.INPUT) // 一旦手动输入 ID 之后 就必须要自己配置 ID
// @TableId(type = IdType.UUID)
// @TableId(type = IdType.ID_WORKER_STR)
private Long id;
private String name;
private Integer age;
private String email;
private Date createTime;
private Date updateTime;
}
  • 方式二 代码级别

    1. 删除 数据库 的默认值 CURRENT_TIMESTAMP 以及更新操作

    2. 在实体类 字段属性上 增加需要的注解
      User

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      package com.example.mybatisplustest.pojo;

      import com.baomidou.mybatisplus.annotation.FieldFill;
      import com.baomidou.mybatisplus.annotation.IdType;
      import com.baomidou.mybatisplus.annotation.TableField;
      import com.baomidou.mybatisplus.annotation.TableId;
      import lombok.AllArgsConstructor;
      import lombok.Data;
      import lombok.NoArgsConstructor;

      import java.util.Date;

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class User {
      // 对应 数据库 中的主键 (uuid 自增id 雪花算法 redis zookeeper)
      @TableId(type = IdType.ID_WORKER)
      // @TableId(type = IdType.AUTO)
      // @TableId(type = IdType.NONE)
      // @TableId(type = IdType.INPUT) // 一旦手动输入 ID 之后 就必须要自己配置 ID
      // @TableId(type = IdType.UUID)
      // @TableId(type = IdType.ID_WORKER_STR)
      private Long id;
      private String name;
      private Integer age;
      private String email;
      // 字段添加填充内容
      @TableField(fill = FieldFill.INSERT)
      private Date createTime;

      @TableField(fill = FieldFill.INSERT_UPDATE)
      private Date updateTime;
      }
    3. 编写 处理器 来处理 字段属性上增加的注解
      MyMetaObjectHandler

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      package com.example.mybatisplustest.handler;

      import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.ibatis.reflection.MetaObject;
      import org.springframework.stereotype.Component;

      import java.util.Date;

      @Slf4j // 添加日志打印
      @Component // 不要忘记将 处理器 托管到 IOC 容器中
      public class MyMetaObjectHandler implements MetaObjectHandler {
      // 插入时的 填充策略
      @Override
      public void insertFill(MetaObject metaObject) {
      log.info("start insert fill....");

      // 面向源码学习
      // setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject) {}
      this.setFieldValByName("createTime", new Date(), metaObject);
      // 插入 数据 时 也需要将更新时间填充
      this.setFieldValByName("updateTime", new Date(), metaObject);
      }

      // 更新时的 填充策略
      @Override
      public void updateFill(MetaObject metaObject) {
      log.info("start update fill....");
      this.setFieldValByName("updateTime", new Date(), metaObject);
      }
      }
    4. 测试 插入 与 更新

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Test
      public void testAutoFill() {
      User user = new User();
      user.setId(1580171623430950914L);
      user.setName("Adidas");
      user.setAge(24);
      user.setEmail("888777999@126.com");

      // int ret = userMapper.insert(user);
      int ret = userMapper.updateById(user);
      System.out.println(ret);
      }

Insert or Update 乐观锁与悲观锁问题详解

乐观锁:处理数据 处理问题十分乐观 认为执行逻辑不会出错 所以做任何操作都不去上锁 如果出现了问题 就再次更新值测试

悲观锁:处理数据 处理问题十分悲观 认为执行逻辑总会出错 所以做任何操作都先去上锁 再执行操作

主要涉及字段: version、 new version

乐观锁机制详解

乐观锁实现方式:

  • 取出记录时,获取当前 version
  • 更新时,带上这个 version
  • 执行更新时, set version = newVersion where version = oldVersion
  • 如果 version 不对,就更新失败
1
2
3
4
5
6
7
8
9
10
-- 实验案例
-- 乐观锁 先查询 获得版本号 version = 1
-- A线程启动
update user set name = "kuangshen", version = version + 1
where id = 2 and version = 1

-- B线程 抢先完成 导致 version = 2 使得 正在执行的 A线程 操作修改失败
update user set name = "kuangshen", version = version + 1
where id = 2 and version = 1

MyBatis-Plus 乐观锁插件的具体使用步骤

  1. 给数据库 相应表 user 添加 version 字段 并 同步实体类
    image-20221013201701736

    1
    2
    @Version    // 乐观锁 version 注解
    private Integer version;
  2. 编写配置类 注册组件
    ==TODO 测试 新版 乐观锁插件 使用方式==
    MyBatisPlusConfig

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.example.mybatisplustest.config;

    import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.transaction.annotation.EnableTransactionManagement;

    // 扫描 mapper 文件夹
    @MapperScan("com.example.mybatisplustest.mapper")
    @EnableTransactionManagement // 显示的开启自动事务管理
    @Configuration // 注册 配置类
    public class MyBatisPlusConfig {
    // 注册乐观锁插件
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    return new OptimisticLockerInterceptor();
    }
    }
  3. 案例测试 乐观锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    @Test
    public void testOptimisticLocker() {
    // 测试乐观锁 成功 update
    // 查询用户信息
    User user = userMapper.selectById(1L);
    // 设置用户信息
    user.setName("Lixiaofeng");
    user.setEmail("33069987@asiainfo.com");
    // 执行更新操作
    userMapper.updateById(user);
    /*
    执行结果 日志
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a1cc163] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@1320809135 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a481728] will not be managed by Spring
    ==> Preparing: SELECT id,name,age,email,version,create_time,update_time FROM user WHERE id=?
    ==> Parameters: 1(Long)
    <== Columns: id, name, age, email, version, create_time, update_time
    <== Row: 1, Lixiaofeng, 18, 33069987@asiainfo.com, 1, 2022-10-12 22:28:33, 2022-10-13 09:13:19
    <== Total: 1
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a1cc163]
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7327a447] was not registered for synchronization because synchronization is not active
    2022-10-13 09:17:39.634 INFO 19220 --- [ main] c.e.m.handler.MyMetaObjectHandler : start update fill....
    JDBC Connection [HikariProxyConnection@876669389 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a481728] will not be managed by Spring
    ==> Preparing: UPDATE user SET name=?, age=?, email=?, version=?, create_time=?, update_time=? WHERE id=? AND version=?
    ==> Parameters: Lixiaofeng(String), 18(Integer), 33069987@asiainfo.com(String), 2(Integer), 2022-10-12 22:28:33.0(Timestamp), 2022-10-13 09:17:39.634(Timestamp), 1(Long), 1(Integer)
    <== Updates: 1
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7327a447]
    可以清晰的看到 插件 帮我们在 update 的最后 添加了 AND version=? 同时 传入了值
    也可以看到 使用 MyBatis-Plus 做 Select 查询语句 会自动回显 到对象属性上
    */
    }

    @Test
    public void testOptimisticLocker2() {
    // 模拟乐观锁 处理多线程冲突

    // 模拟线程 1
    User user = userMapper.selectById(1L);
    user.setName("Lixiaofeng111");
    user.setEmail("33069987@asiainfo.com");

    // 模拟线程 2 执行插队操作
    User user2 = userMapper.selectById(1L);
    user2.setName("Lixiaofeng222");
    user2.setEmail("33069987@asiainfo.com");
    userMapper.updateById(user2);

    // 如果要使 这条 update 语句生效 需要使用 自旋锁 TODO
    userMapper.updateById(user); // 如果没有 乐观锁 就会覆盖插队线程的值 所以此条语句 并不会执行出 update 效果 会被 乐观锁 拦截
    }

Select

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void testSelect() {
// 测试查询
User user = userMapper.selectById(1L);
System.out.println(user);

System.out.println("-----------------");

// 测试批量查询
List<User> userList = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
userList.forEach(System.out::println);

System.out.println("------------------");

// 简单条件查询 map操作
HashMap<String, Object> map = new HashMap<>();
// 自定义查询条件
map.put("name", "Jack");
map.put("age", 20);
List<User> userList2 = userMapper.selectByMap(map);
userList2.forEach(System.out::println);
}

分页查询

分页操作

  1. 原始 limit 分页
  2. pageHelper 第三方插件 实现分页
  3. MyBatis-Plus 内置分页插件

MyBatis-Plus 内置分页插件使用步骤

  1. 配置分页组件拦截器
    MyBatisPlusConfig

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.example.mybatisplustest.config;

    import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.transaction.annotation.EnableTransactionManagement;

    // 扫描 mapper 文件夹
    @MapperScan("com.example.mybatisplustest.mapper")
    @EnableTransactionManagement // 显示的开启自动事务管理
    @Configuration // 注册 配置类
    public class MyBatisPlusConfig {
    // 注册乐观锁插件
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    return new OptimisticLockerInterceptor();
    }

    // 注册分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
    return new PaginationInterceptor();
    }
    }
  2. 测试分页插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    public void testPagination() {
    // 测试分页查询
    // 参数: 1 --当前页 2 --一页展示数据条数
    Page<User> page = new Page<>(1, 5);
    userMapper.selectPage(page, null);
    page.getRecords()
    .forEach(System.out::println);
    // 使用分页 插件可以极大的简化 分页工作
    // TODO 学习 page类 的具体方法
    }

Delete

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testDelete() {
// 测试删除
userMapper.deleteById(1580171623430950915L);

// 测试通过 id 批量删除
userMapper.deleteBatchIds(Arrays.asList(1580171623430950913L, 1580171623430950914L));

// 测试通过 map 删除
HashMap<String, Object> map = new HashMap<>();
map.put("name", "ddd");
// TODO 在理解 自增id的问题 SQLyog 写入数据条目 id 后会被改变 故而研究 MyBatis-Plus 实现数据时 改变 ID 的流程时间
userMapper.deleteByMap(map);
}

逻辑删除

物理删除:使用 delete 从数据库中直接 移除

逻辑删除:并不会在数据库中直接移除 而是通过一个变量来控制其是否生效 deleted = 0 -> deleted = 1

实际应用 管理员可直接查看被删除的记录 使用逻辑删除可以防止数据丢失 类似于 回收站

具体操作步骤及测试:

  1. 在数据库中增加 deleted 字段 并在 实体类上添加相应属性 同时添加逻辑删除注解
    image-20221013110317209

    1
    2
    @TableLogic
    private Integer deleted;
  2. 配置逻辑删除组件
    MyBatisPlusConfig

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    package com.example.mybatisplustest.config;

    import com.baomidou.mybatisplus.core.injector.ISqlInjector;
    import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
    import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.transaction.annotation.EnableTransactionManagement;

    // 扫描 mapper 文件夹
    @MapperScan("com.example.mybatisplustest.mapper")
    @EnableTransactionManagement // 显示的开启自动事务管理
    @Configuration // 注册 配置类
    public class MyBatisPlusConfig {
    // 注册乐观锁插件
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    return new OptimisticLockerInterceptor();
    }

    // 注册分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
    return new PaginationInterceptor();
    }

    // 注册逻辑删除组件
    @Bean
    public ISqlInjector iSqlInjector() {
    return new LogicSqlInjector();
    }
    }
  3. 编写逻辑删除配置

    application.properties

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # mysql 5 与 mysql 8 驱动不相同 com.mysql.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

    # mysql 8 使用驱动 com.mysql.cj.jdbc.Driver 设置时区 serverTimezone=GMT%2B8

    # 配置 mybatis-plus 日志
    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

    # 配置逻辑删除
    mybatis-plus.global-config.db-config.logic-delete-value=1
    mybatis-plus.global-config.db-config.logic-not-delete-value=0
  4. 测试配置好逻辑删除后的各种删除方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    @Test
    public void testLogicDelete() {
    // 测试逻辑删除
    userMapper.deleteById(1L);

    // 测试通过 id 批量逻辑删除
    userMapper.deleteBatchIds(Arrays.asList(2L, 3L));

    // 测试通过 map 逻辑删除
    HashMap<String, Object> map = new HashMap<>();
    map.put("name", "Billie");
    // TODO 在理解 自增id的问题 SQLyog 写入数据条目 id 后会被改变 故而研究 MyBatis-Plus 实现数据时 改变 ID 的流程时间
    userMapper.deleteByMap(map);

    /*
    执行结果日志
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4eb9f2af] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@268258490 wrapping com.mysql.cj.jdbc.ConnectionImpl@5f1483fd] will not be managed by Spring
    ==> Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0
    ==> Parameters: 1(Long)
    <== Updates: 1
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4eb9f2af]
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65bad087] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@1251133097 wrapping com.mysql.cj.jdbc.ConnectionImpl@5f1483fd] will not be managed by Spring
    ==> Preparing: UPDATE user SET deleted=1 WHERE id IN ( ? , ? ) AND deleted=0
    ==> Parameters: 2(Long), 3(Long)
    <== Updates: 2
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65bad087]
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4cb702ce] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@397639322 wrapping com.mysql.cj.jdbc.ConnectionImpl@5f1483fd] will not be managed by Spring
    ==> Preparing: UPDATE user SET deleted=1 WHERE name = ? AND deleted=0
    ==> Parameters: Billie(String)
    <== Updates: 1
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4cb702ce]

    可以直观看出 虽然实际调用的是 delete 方法 但是在配置了逻辑删除后 实际上执行的是 update sql
    同时 配置逻辑查询后 再执行查询 会自动过滤被删除的字段 --MyBatis-Plus 的强大之处

    */
    }
  5. 执行查询测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Test
    void contextLoads() {
    // 查询所有用户
    // 参数为 Wrapper --条件构造器 不使用 可为 null
    List<User> userList = userMapper.selectList(null);
    // Assert.isTrue();
    userList.forEach(System.out::println);

    /*
    配置逻辑删除插件 逻辑删除后的 执行结果日志
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@76d72b5b] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@1719727892 wrapping com.mysql.cj.jdbc.ConnectionImpl@53b1a3f8] will not be managed by Spring
    ==> Preparing: SELECT id,name,age,email,version,create_time,update_time,deleted FROM user WHERE deleted=0
    ==> Parameters:
    <== Columns: id, name, age, email, version, create_time, update_time, deleted
    <== Row: 4, Sandy, 21, test4@baomidou.com, 1, 2022-10-12 22:28:33, 2022-10-12 22:28:33, 0
    <== Row: 6, kaungshen, 12, 983443816@qq.com, 1, 2022-10-12 22:28:33, 2022-10-12 22:28:33, 0
    <== Total: 2
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@76d72b5b]
    User(id=4, name=Sandy, age=21, email=test4@baomidou.com, version=1, createTime=Wed Oct 12 22:28:33 CST 2022, updateTime=Wed Oct 12 22:28:33 CST 2022, deleted=0)
    User(id=6, name=kaungshen, age=12, email=983443816@qq.com, version=1, createTime=Wed Oct 12 22:28:33 CST 2022, updateTime=Wed Oct 12 22:28:33 CST 2022, deleted=0)

    自动拼接匹配逻辑删除

    */
    }

5. 性能分析插件

常规开发中 经常会遇到一些 慢SQL 通过 测试 压测工具 Druid …. 方式 进行查询并优化

处理此类情况 MyBatis-Plus 同样提供 性能分析 插件 当执行 SQL 时间超过这个 规定时间 就会强行停止运行

作用:性能分析拦截器 用于输出 每条 SQL 语句的执行时间

具体操作步骤

  1. 配置性能分析组件 的配置类
    MyBatisPlusConfig

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.example.mybatisplustest.config;

    import com.baomidou.mybatisplus.core.injector.ISqlInjector;
    import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
    import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import org.springframework.transaction.annotation.EnableTransactionManagement;

    // 扫描 mapper 文件夹
    @MapperScan("com.example.mybatisplustest.mapper")
    @EnableTransactionManagement // 显示的开启自动事务管理
    @Configuration // 注册 配置类
    public class MyBatisPlusConfig {
    // 注册乐观锁插件
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    return new OptimisticLockerInterceptor();
    }

    // 注册分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
    return new PaginationInterceptor();
    }

    // 注册逻辑删除组件
    @Bean
    public ISqlInjector iSqlInjector() {
    return new LogicSqlInjector();
    }

    // SQL 执行效率插件
    @Bean
    @Profile({"dev", "test"}) // 设置 仅在 dev test 环境下开启 --保证正常上线时 没有多余的性能损耗
    public PerformanceInterceptor performanceInterceptor() {
    return new PerformanceInterceptor();
    }
    }

    配置开发环境

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 设置开发环境
    spring.profiles.active=dev

    # mysql 5 与 mysql 8 驱动不相同 com.mysql.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

    # mysql 8 使用驱动 com.mysql.cj.jdbc.Driver 设置时区 serverTimezone=GMT%2B8

    # 配置 mybatis-plus 日志
    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

    # 配置逻辑删除
    mybatis-plus.global-config.db-config.logic-delete-value=1
    mybatis-plus.global-config.db-config.logic-not-delete-value=测试性能使用
  2. 配置 SQL 执行效率插件 参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // SQL 执行效率插件
    @Bean
    @Profile({"dev", "test"}) // 设置 仅在 dev test 环境下开启 --保证正常上线时 没有多余的性能损耗
    public PerformanceInterceptor performanceInterceptor() {
    PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
    performanceInterceptor.setMaxTime(1); // ms 设置 SQL 最大执行时间 如果超时 则不执行
    performanceInterceptor.setFormat(true); // 是否开启 格式化支持
    return performanceInterceptor;
    }
  3. 测试 查询等 SQL 语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    @Test
    void contextLoads() {
    // 查询所有用户
    // 参数为 Wrapper --条件构造器 不使用 可为 null
    List<User> userList = userMapper.selectList(null);
    // Assert.isTrue();
    userList.forEach(System.out::println);

    /*
    配置 SQL 性能检测后 的执行结果日志
    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7c37f145] was not registered for synchronization because synchronization is not active
    JDBC Connection [HikariProxyConnection@1955226954 wrapping com.mysql.cj.jdbc.ConnectionImpl@1d8b0500] will not be managed by Spring
    ==> Preparing: SELECT id,name,age,email,version,create_time,update_time,deleted FROM user WHERE deleted=0
    ==> Parameters:
    <== Columns: id, name, age, email, version, create_time, update_time, deleted
    <== Row: 4, Sandy, 21, test4@baomidou.com, 1, 2022-10-12 22:28:33, 2022-10-12 22:28:33, 0
    <== Row: 6, kaungshen, 12, 983443816@qq.com, 1, 2022-10-12 22:28:33, 2022-10-12 22:28:33, 0
    <== Total: 2
    Time:16 ms - ID:com.example.mybatisplustest.mapper.UserMapper.selectList
    Execute SQL:
    SELECT
    id,
    name,
    age,
    email,
    version,
    create_time,
    update_time,
    deleted
    FROM
    user
    WHERE
    deleted=0

    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7c37f145]

    org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
    ### Error querying database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: The SQL execution time is too large, please optimize !
    ### The error may exist in com/example/mybatisplustest/mapper/UserMapper.java (best guess)
    ### The error may involve com.example.mybatisplustest.mapper.UserMapper.selectList
    ### The error occurred while handling results
    ### SQL: SELECT id,name,age,email,version,create_time,update_time,deleted FROM user WHERE deleted=0
    ### Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: The SQL execution time is too large, please optimize !

    Caused by: org.apache.ibatis.exceptions.PersistenceException:
    ### Error querying database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: The SQL execution time is too large, please optimize !
    ### The error may exist in com/example/mybatisplustest/mapper/UserMapper.java (best guess)
    ### The error may involve com.example.mybatisplustest.mapper.UserMapper.selectList
    ### The error occurred while handling results
    ### SQL: SELECT id,name,age,email,version,create_time,update_time,deleted FROM user WHERE deleted=0
    ### Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: The SQL execution time is too large, please optimize !

    可以清晰看到 格式化 后的 SQL 语句 以及对比 SQL 执行的时间
    */
    }

==使用性能分析插件能很好的规避 慢SQL 分析效率问题==

6. 条件构造器

主要用于高级条件操作 代替 SQL 语句中的繁琐条件规则

大型企业中的 Git 都是内部化 非公开的 通常是 购买或者内部搭建的

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@Test
public void testWrapper() {
// 需求 查询 name 不为空 Email 不为空 age 大于 18 的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 对比 map 学习
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age", 18);

userMapper.selectList(wrapper).forEach(System.out::println);
/*
查询结果日志
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@631cb129] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1878045132 wrapping com.mysql.cj.jdbc.ConnectionImpl@3b55dd15] will not be managed by Spring
==> Preparing: SELECT id,name,age,email,version,create_time,update_time,deleted FROM user WHERE deleted=0 AND name IS NOT NULL AND email IS NOT NULL AND age >= ?
==> Parameters: 18(Integer)
<== Columns: id, name, age, email, version, create_time, update_time, deleted
<== Row: 4, Sandy, 21, test4@baomidou.com, 1, 2022-10-12 22:28:33, 2022-10-12 22:28:33, 0
<== Row: 15801716234310, Liyang, 28, 1031222691@qq.com, 1, 2022-10-13 11:57:01, 2022-10-13 11:57:01, 0
<== Row: 15801716234311, Lizhengzuo, 38, 1031222691@qq.com, 1, 2022-10-13 11:57:17, 2022-10-13 11:57:17, 0
<== Total: 3
Time:14 ms - ID:com.example.mybatisplustest.mapper.UserMapper.selectList
Execute SQL:
SELECT
id,
name,
age,
email,
version,
create_time,
update_time,
deleted
FROM
user
WHERE
deleted=0
AND name IS NOT NULL
AND email IS NOT NULL
AND age >= 18

Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@631cb129]
User(id=4, name=Sandy, age=21, email=test4@baomidou.com, version=1, createTime=Wed Oct 12 22:28:33 CST 2022, updateTime=Wed Oct 12 22:28:33 CST 2022, deleted=0)
User(id=15801716234310, name=Liyang, age=28, email=1031222691@qq.com, version=1, createTime=Thu Oct 13 11:57:01 CST 2022, updateTime=Thu Oct 13 11:57:01 CST 2022, deleted=0)
User(id=15801716234311, name=Lizhengzuo, age=38, email=1031222691@qq.com, version=1, createTime=Thu Oct 13 11:57:17 CST 2022, updateTime=Thu Oct 13 11:57:17 CST 2022, deleted=0)
*/
}

@Test
public void testWrapper2() {
// 根据 name 查询
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "yupi");
// 查询一个数据
User user = userMapper.selectOne(wrapper);
System.out.println(user);

System.out.println("----------------");

// 查询 age 在 20 - 30 之间的用户
QueryWrapper<User> wrapper2 = new QueryWrapper<>();
wrapper2.between("age", 20, 30);
Integer count = userMapper.selectCount(wrapper2);// 查询结果计数
System.out.println(count);
userMapper.selectList(wrapper2).forEach(System.out::println);// 查询结果

System.out.println("----------------");

// 模糊查询
QueryWrapper<User> wrapper3 = new QueryWrapper<>();
wrapper3.notLike("name", "L")
.likeRight("email", "t");
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper3);
// Map中 结果如何排序问题
maps.forEach(System.out::println);

System.out.println("-----------------");

// 子查询
QueryWrapper<User> wrapper4 = new QueryWrapper<>();
// id 在子查询中
wrapper4.inSql("id", "select id from user where id < 3");

userMapper.selectObjs(wrapper4).forEach(System.out::println);

System.out.println("-----------------");

QueryWrapper<User> wrapper5 = new QueryWrapper<>();
// 通过 id 进行排序
wrapper5.orderByDesc("id");
userMapper.selectList(wrapper5).forEach(System.out::println);
}

大学学人傻了 Spring + Struts +Hibernate (全自动 不支持定制化 国外使用较多)

7. 代码生成器

参考代码

LinCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.example.mybatisplustest;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

import java.util.ArrayList;

// 代码 自动生成器
public class LinCode {
public static void main(String[] args) {
// 需要构建一个 代码自动生成器 对象
AutoGenerator mpg = new AutoGenerator();
// 配置策略

// 1. 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("Liny");
gc.setOpen(false);
gc.setFileOverride(false); // 是否覆盖
gc.setServiceName("%sService"); //去 Service 的 I 前缀
gc.setIdType(IdType.ID_WORKER);
gc.setDateType(DateType.ONLY_DATE);
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);

// 2. 设置数据源
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf-8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);

// 3. 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("blog");
pc.setParent("com.lin");
pc.setEntity("entity");
pc.setMapper("mapper");
pc.setService("service");
pc.setController("controller");
mpg.setPackageInfo(pc);

// 4. 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("user"); // 设置需要映射的表名
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setSuperEntityClass("自己的父类实体 没有就不用设置");
strategy.setEntityLombokModel(true); // 自动lombok
// strategy.setRestControllerStyle(true); // RestFul Controller
strategy.setLogicDeleteFieldName("deleted"); // 配置逻辑删除

// 自动填充配置
TableFill gmtCreate = new TableFill("gmt_create", FieldFill.INSERT);
TableFill gmtModified = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE);
ArrayList<TableFill> tableFills = new ArrayList<>();
tableFills.add(gmtCreate);
tableFills.add(gmtModified);
strategy.setTableFillList(tableFills);

// 乐观锁
strategy.setVersionFieldName("version");
strategy.setRestControllerStyle(true);
strategy.setControllerMappingHyphenStyle(true); // localhost:8080/hello_id_2 使用下划线命名
mpg.setStrategy(strategy);

// 执行
mpg.execute();
}
}

DataStructure

预科知识

ArrayList继承树

image-20221105021446746

Collection

线性结构

顺序结构

静态数组

特点

  1. 数组长度一旦确定不可改变
  2. 数组只能存储同一类型数据
  3. 数组中每个存储空间地址连续且相等
  4. 数组使用索引方式调用访问元素

线性表 ( 动态数组 )

底层数据结构

  • 静态数组

核心操作

  • 扩缩容
    • int newCapacity = oldCapacity + (oldCapacity >> 1); –扩容至 1.5 倍
    • ArrayList没有自动缩容机制。无论是remove方法还是clear方法,它们都不会改变现有数组elementData的长度。但是它们都会把相应位置的元素设置为null,以便垃圾收集器回收掉不使用的元素,节省内存。ArrayList的缩容,需要我们自己手动去调用trimToSize()方法,达到缩容的目的
    • 我们 设计 自动缩容 满足合理性 当 ArrayList 的 size == length / 4 的时候 进行缩容 缩容至 length / 2
  • 增删
  • 排序 –插入排序

手写代码实现

List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package P1.Interface;

import java.util.Comparator;

public interface List<E> extends Iterable<E> {
//默认在表尾添加一个元素
public void add(E element);

//在指定索引处添加元素
public void add(int index, E element);

//删除指定元素
public void remove(E element);

//删除指定索引处的元素,并返回原先值
public E remove(int index);

//获取指定索引处的元素
public E get(int index);

//修改指定索引index处的值为element 并返回原先的值
public E set(int index, E element);

//获取线性表中的元素个数
public int size();

//查看元素第一次出现的索引位置(从左到右)
public int indexOf(E element);

//判断是否包含元素
public boolean contains(E element);

//判断线性表是否为空
public boolean isEmpty();

//清空线性表
public void clear();

//按照比较器的内容进行排序
public void sort(Comparator<E> c);

//获取子线性表 原线性表中[fromIndex, toIndex)这个部分
public List<E> subList(int fromIndex, int toIndex);
}

ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
package P2.linearStructure;

import P1.Interface.List;

import java.util.Comparator;
import java.util.Iterator;

//自定义的线性表的顺序存储结构
public class ArrayList<E> implements List<E> {
//数组的容器 data.length 指的就是当前数组的容量
private E[] data;

//元素的个数 size == 0 表示线性表为空表 size == data.length 表示线性表满了
//size 还表示新元素默认尾部添加是所存储的索引
private int size;

//默认容量
private static final int DEFAULT_CAPACITY = 10;

//默认构造函数:创建一个默认容量为10 的线性表
public ArrayList() {
data = (E[]) new Object[DEFAULT_CAPACITY];
size = 0;
}

//指定默认容量的构造函数:创建一个指定容量的线性表
public ArrayList(int capacity) {

if (capacity > 0) {
this.data = (E[]) new Object[capacity];
} else if (capacity == 0) {
this.data = (E[]) new Object[capacity];
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
capacity);
}

}

//指定数组的构造函数:传入一个数组将该数组封装称为一个线性表
//[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]

public ArrayList(E[] arr) {
//data不能直接引用外部传入的数组arr
//否则外部对arr的修改会引起ArrayList内部的一些问题
//data是ArrayList内部的数据
if (arr == null || arr.length == 0) {
throw new IllegalArgumentException("arr can not be null");
}
data = (E[]) new Object[DEFAULT_CAPACITY];
for (int i = 0; i < arr.length; i++) {
add(arr[i]);
}
}

@Override
public void add(E element) {
add(size, element); //复用add
}

@Override
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add index cross the border");
}

//判断线性表是否是满状态
if (size == data.length) {
resize(2 * data.length);
}

//设置i从size-1开始到index,逐一向后移动元素
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
//将新元素插入到指定位置
data[index] = element;
//最大长度+1
size++;
}

//扩容或缩容 操作 不应该向外界提供
private void resize(int newLen) {
E[] newData = (E[]) new Object[newLen];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}

@Override
public void remove(E element) {//删除指定元素 仅删除第一次出现的 E or// 删除所有指定元素
// 仅删除第一次出现的 E
int index = indexOf(element);
if (index != -1) {
remove(index);
}
}

@Override
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("remove index out of range");
}
//先保存要删除的值
E ret = data[index];

//移动元素
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
size--;

//什么时候去缩容 当容量到达默认容量则缩容的意义就变得很小
//当小宇等于默认容量时,就不需要缩容了
//条件1、当前有效元素是容量的1/4
//条件2、当前容量不得小于等于默认容量
if (size == data.length / 4 && data.length > DEFAULT_CAPACITY) {
resize(data.length / 2);
}

return ret;
}

@Override
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("get index out of range ");
}
return data[index];
}

@Override
public E set(int index, E element) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("set index out of range ");
}
E ret = data[index];
data[index] = element;
return ret;
}

@Override
public int size() {
return this.size;
}

//额外添加函数:获取当前线性表的容量
private int getCapacity() {
return data.length;
}



@Override
public int indexOf(E element) {

/*
== 比较的是,主要看等号两边是什么
== 两边是基本数据类型,比较的是值
byte short int long
float double
char boolean
== 两边是引用数据类型,比较的是地址
数组 字符串 其他类对象
*/
for (int i = 0; i < size; i++) {
if (data[i].equals(element)) {
return i;
}
}
return -1;
}

@Override
public boolean contains(E element) {
return indexOf(element) != -1;
}

@Override
public boolean isEmpty() {
return size == 0;
}

@Override
public void clear() {
data = (E[]) new Object[DEFAULT_CAPACITY];
size = 0;
}

@Override
public void sort(Comparator<E> c) {
/*
list.sort((o1, o2) -> o1 - o2);
*/
if (c == null) {
throw new IllegalArgumentException("comparator can not be null");
}

for (int i = 1; i < size; i++) {
E e = data[i];
int j = i;
for ( j = i; j > 0 && c.compare(data[j - 1], e) > 0; j--) {
data[j] = data[j - 1];
}
data[j] = e;
}
}

@Override
public List<E> subList(int fromIndex, int toIndex) {
// [fromIndex, toIndex)
if (fromIndex < 0 || toIndex >= size ||fromIndex > toIndex) {
throw new IllegalArgumentException("must 0 <= fromIndex <= toIndex <= size");
}
List<E> list = new ArrayList<>();
for (int i = fromIndex; i <= toIndex; i++) {
// 考虑深浅拷贝问题
list.add(data[i]);
}
return list;
}

@Override
//equals比较两个ArrayList是否相等
public boolean equals(Object o) {
//条件1、判空
if (o == null) {
return false;
}
//条件2、判断是否为自己
if (this == o) {
return true;
}
//条件3、判断类型是否相同
if (o instanceof ArrayList) {
//条件4、按照自己的逻辑进行比较
ArrayList<E> other = (ArrayList<E>) o;
//条件5、先比较有效元素个数
if (this.size != other.size) {
return false;
}
//条件6、有效元素个数相等,逐个比较元素
for (int i = 0; i < size; i++) {
// 考虑 判空
if (!data[i].equals(other.data[i])) {
return false;
}
}
return true;
}
return false;
}

/*
[1, 2, 3, 4, 5, 6]
Arrays.toString(arr); Arrays 类的一个静态方法
*/

@Override
public String toString() {
//为什么用stringbuilder拼接,因为用string拼接会产生不必要的字符类
StringBuilder sb = new StringBuilder();
sb.append('[');
if (isEmpty()) {
sb.append(']');
} else {
for (int i = 0; i < size; i ++) {
sb.append(data[i]);
if (i == size - 1) {
sb.append(']');
} else {
sb.append(',');
sb.append(' ');
}
}
}
return sb.toString();
}

@Override
//迭代器的作用,不论底层是什么逻辑结构,讲其中元素逐个提取出
//获取当前数据结构/容器 的 迭代器
//通过迭代器 我们能更方便的挨个取出 每个元素
//同时 Arraylist实现Iterator 接口可以使当前的 数据结构/容器 被foreach循环遍历
public Iterator<E> iterator() {
return new ArraylistIterator();
}

//创建一个属于ArrayList的迭代器
class ArraylistIterator implements Iterator<E> {
private int cur = 0;

@Override
public boolean hasNext() {
return cur < size;
}

@Override
public E next() {
return data[cur++];
}
}

public void swap(int i, int j) {
E tmp = data[i];
data[i] = data[j];
data[j] = tmp;
}
}

注意

  • 如果数组为 null 则不能调用 arr.length –> NPE
  • 数组的地址存储 –Java中只能获取 数组的
  • 集合泛型必须是引用类型,不能是基本类型 –存储基本数据,那么就要使用基本数据类型对应的“包装类”(全都位于java.lang下,有8种)–自动拆装箱机制
    2^31 –2147483648
  • 排序使用 插入排序 插入排序 保证 左边有序 新值不断插入左边正确位置
  • 内部类 与 匿名内部类 –在一个java文件中,可以有多个平级的类。内部类的定义是:在一个类的内部再定义一个类,那么此时这个类就称之为内部类,可以理解为成员内部类。内部类可以直接访问外部类的成员,包括私有成员。外部类要访问内部类的成员(成员变量,成员方法),必须要建立内部类的对象(定义内部类方法,访问内部类成员)。
  • 函数定义传入一个比较器 ——接口定义形式 可传入的形式有 ==匿名内部类 lambda表达式 函数式方法== 三种方式

栈性质

  • 只能在表尾 –栈顶 进行增删操作
  • FILO

底层数据结构

  • 线性表 ArrayList

手写代码实现

Stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package P1.Interface;

public interface Stack<E> extends Iterable<E> {
public int size();
public boolean isEmpty();
//入栈 进栈 一个元素 在线性表的表尾添加一个元素
public void push(E element);
//出栈 弹出 一个元素 在线性表的表尾删除一个元素
public E pop();
//查看当前栈顶元素
public E peek();
//清空栈
public void clear();

}

ArrayStack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package P2.linearStructure;

import P1.Interface.Stack;

import java.util.Iterator;

public class ArrayStack<E> implements Stack<E> {
private ArrayList<E> list;

public ArrayStack() {
list = new ArrayList<>();
}

public ArrayStack(int capacity) {
list = new ArrayList<>(capacity);
}

@Override
public int size() {
return list.size();
}

@Override
public boolean isEmpty() {
return list.isEmpty();
}

@Override
public void push(E element) {
list.add(element);
}

@Override
public E pop() {
return list.remove(list.size() - 1);
}

@Override
public E peek() {
return list.get(list.size() - 1);
}

@Override
public void clear() {
list.clear();
}

@Override
public Iterator<E> iterator() {
return list.iterator();
}

@Override
public String toString() {
return list.toString();
}

@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (o instanceof ArrayStack) {
ArrayStack other = (ArrayStack) o;
return this.list.equals(other.list);
}
return false;
}
}

注意:

  • 栈底层由 ArrayList 实现

栈的相关应用

  • 进制转换
    • 十进制转十六进制
    • 十六进制转十进制
  • 括号匹配问题
  • 中缀表达式 中缀计算器
  • 后缀表达式 后缀计算器
  • 判断回文

DecToHex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package P2.linearStructure;

//十进制转十六进制
public class DecToHex {
public static void main(String[] args) {
int num = 654321;
ArrayStack<String> stack = new ArrayStack<>();
while (num != 0) {
int a = num % 16;
if (a < 10) {
stack.push(a + "");
} else {
//10 - A 11 - B 12 - C 13 - D 14 - E 15 - F
// 65 66 67 68 69 70
stack.push((char)(a + 55) + "");
}
num /= 16;
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
System.out.println(sb.toString());
}
}

HexToDec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package P2.linearStructure;

//十六进制 转 十进制
public class HexToDec {
public static void main(String[] args) {
String hex = "9FBF1";
ArrayStack<Character> stack = new ArrayStack<>();
for (int i = 0; i < hex.length(); i++) {
stack.push(hex.charAt(i));
}
int sum = 0;
int mi = 0;
while (!stack.isEmpty()) {
//9 F B F 1
// 可以使用 Character.isDigit 方法 判断是否是 数字
char c = stack.pop(); // '1'
sum += getNumber(c) * Math.pow(16, mi);
mi++;
}
System.out.println(sum);
}

private static int getNumber(char c) {
if (!(c >= '0' && c <= '9' || c >= 'A' && c <= 'Z')) {
throw new IllegalArgumentException("Wrong char !");
}
if (c >= '0' && c <= '9') {
return c - '0';
} else {
return c - 'A' + 10;
}
}
}

ParenthesisMatching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package P2.linearStructure;

import java.util.HashMap;

//括号匹配问题
public class ParenthesisMatching {
public static void main(String[] args) {
solution01();
solution02();
}

// 使用 HashMap 匹配
private static void solution02() {
String str = "{}";
HashMap<Character,Character> map = new HashMap<>();
map.put('[',']');
map.put('<','>');
map.put('(',')');
map.put('{','}');
ArrayStack<Character> stack = new ArrayStack<>();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (stack.isEmpty()) {
stack.push(c);
} else {
char top = stack.peek();
if (map.containsKey(top) && c == map.get(top)) {
stack.pop();
} else {
stack.push(c);
}
}
}
System.out.println(stack.isEmpty());
}

private static void solution01() {
String str = "{()[[()]]<>{}()<>}()";
ArrayStack<Character> stack = new ArrayStack<>();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (stack.isEmpty()) {
stack.push(c);
} else {
char top = stack.peek();
if (top - c == -1 || top - c == -2) {
stack.pop();
} else {
stack.push(c);
}
}
}
System.out.println(stack.isEmpty());
}
}

InfixToSuffix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package P2.linearStructure;

public class InfixToSuffix {
public static void main(String[] args) {
String expression = "(10+20/2*3)/2+8";
String suffixExpression = infixToSuffix(expression);
System.out.println(suffixExpression);
}

public static String infixToSuffix(String expression) {

//操作符栈
ArrayStack<String> opStack = new ArrayStack<>();

//后缀表达的线性表
ArrayList<String> suffixList = new ArrayList<>();

//格式化字符串
expression = insertBlanks(expression);
String[] tokens = expression.split(" ");
for (String token : tokens) {
if (token.length() == 0) {
continue;
}

//判断操作符+-*/
if (isOperator(token)) {
/*
什么时候操作符进栈?
1、栈为空
2、栈顶是(
3、栈顶是操作符 且优先级比当前token小
什么时候需要出栈?
1、栈顶操作符的优先级 >= 当前token
2、
*/

while (true) {
if (opStack.isEmpty() || opStack.peek().equals("(") || priority(opStack.peek()) < priority(token)) {
opStack.push(token);
break;
}
suffixList.add(opStack.pop());
}
} else if (token.equals("(")) {
opStack.push(token);
} else if (token.equals(")")) {
while (!opStack.peek().equals("(")) {
suffixList.add(opStack.pop());
}
opStack.pop();
} else if (isNumber(token)) {
suffixList.add(token);
} else {
throw new IllegalArgumentException("Wrong char!" + expression);
}
}
while (!opStack.isEmpty()) {
suffixList.add(opStack.pop());
}
//将数字元素和操作符元素进行拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i< suffixList.size(); i ++) {
sb.append(suffixList.get(i));
sb.append(' ');
}
return sb.toString();
}

private static int priority(String token) {
if (token.equals("+") || token.equals("-")) {
return 0;
}

if (token.equals("*") || token.equals("/")) {
return 1;
}
return -1;

}

private static boolean isNumber(String token) {
return token.matches("\\d+");
}

private static boolean isOperator(String token) {
return token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/");
}

private static String insertBlanks(String expression) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (c == '(' || c == ')' || c == '+' || c == '-' || c == '*' || c == '/' ) {
sb.append(' ');
sb.append(c);
sb.append(' ');
} else {
sb.append(c);
}
}
return sb.toString();
}
}

JudgingPalindrome

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package P2.linearStructure;

//判断回文
public class JudgingPalindrome {
public static void main(String[] args) {

solution01();
System.out.println(solution02());
}

private static boolean solution02() {
String text = "上海自来水来自海上";
int i = 0;
int j = text.length() - 1;
while (true) {
if (text.charAt(i) == text.charAt(j)) {
i++;
j--;
} else {
return false;
}
if (j <= i) {
return true;
}
}
}

private static void solution01() {
String text = "上海自来水来自海上";
ArrayStack<Character> stack = new ArrayStack<>();
for (int i = 0; i < text.length(); i++) {
if (text.length() % 2 == 1 && i == text.length() / 2) {
continue;
}
char c = text.charAt(i);
if (stack.isEmpty()) {
stack.push(c);
} else {
if (c != stack.peek()) {
stack.push(c);
} else {
stack.pop();
}
}
}
System.out.println(stack.isEmpty());
}
}

双端栈

双端栈性质

  • FIFO
  • 使用两个栈顶指针辅助操作
  • 两端都是栈底 两端填充数据不断向中间填充 直至填满扩容
  • 入栈与出栈的时间复杂度都为 O(1)

底层数据结构

  • 静态数组

手写代码实现

ArrayDoubleEndStack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package P2.linearStructure;

import java.util.Iterator;

//双端栈
public class ArrayDoubleEndStack<E> implements Iterable<E> {

//左端栈的栈顶
private int ltop;
//右端栈的栈顶
private int rtop;
//存储数据的容器
private E[] data;
//数组容器的默认容量
private static int DEFAULT_CAPACITY = 10;

public ArrayDoubleEndStack() {
data = (E[]) new Object[DEFAULT_CAPACITY];
ltop = -1;
rtop = data.length;
}

public void pushLeft(E element) {
//判断双端栈是否满栈
if (ltop + 1 == rtop) {
resize(data.length * 2);
}
data[++ltop] = element;
}

public void pushRight(E element) {
if (ltop + 1 == rtop) {
resize(data.length * 2);
}
data[--rtop] = element;
}

private void resize(int newLen) {
/*
关于双端栈的 扩容 和 缩容
遍历左边索引不变
遍历右边索引增加 扩容个长度
*/
E[] newData = (E[]) new Object[newLen];
//复制左端栈元素
for (int i = 0; i <= ltop; i++) {
newData[i] = data[i];
}
//复制右端栈元素
int index = rtop;
for (int i = newLen - sizeRight(); i < newLen; i++) {
newData[i] = data[index++];
}
rtop = newLen - sizeRight();
data = newData;
}

public E popLeft() {
if (isLeftEmpty()) {
throw new IllegalArgumentException("left stack is null");
}
E ret = data[ltop--];
if (sizeLeft() + sizeRight() <= data.length / 4 && data.length > DEFAULT_CAPACITY) {
resize(data.length / 2);
}
return ret;
}

public E popRight() {
if (isRightEmpty()) {
throw new IllegalArgumentException("right stack is null");
}
E ret = data[rtop++];
if (sizeLeft() + sizeRight() <= data.length / 4 && data.length > DEFAULT_CAPACITY) {
resize(data.length / 2);
}
return ret;
}

public E peekLeft() {
if (isLeftEmpty()) {
throw new IllegalArgumentException("left stack is null");
}
return data[ltop];
}

public E peekRight() {
if (isRightEmpty()) {
throw new IllegalArgumentException("right stack is null");
}
return data[rtop];
}

public boolean isLeftEmpty() {
return ltop == -1;
}

public boolean isRightEmpty() {
return rtop == data.length;
}

public int sizeLeft() {
return ltop + 1;
}

public int sizeRight() {
return data.length - rtop;
}

@Override
public String toString() {
//1 2 3 7 8 9
StringBuilder sb = new StringBuilder();
sb.append('[');
if (isLeftEmpty() && isRightEmpty()) {
sb.append(']');
return sb.toString();
}
//先解决左边
for (int i = 0; i <= ltop; i++) {
sb.append(data[i]);
if (i == ltop && isRightEmpty()) {
sb.append(']');
} else {
sb.append(',');
}
}
//再解决右边
for (int i = rtop; i < data.length; i++) {
sb.append(data[i]);
if (i == data.length - 1) {
sb.append(']');
} else {
sb.append(',');
}
}
return sb.toString();
}

@Override
public Iterator<E> iterator() {
return new ArrayDoubleEndStackIterator();
}

class ArrayDoubleEndStackIterator implements Iterator<E> {
private ArrayList<E> list;
private Iterator<E> it;
public ArrayDoubleEndStackIterator() {
list = new ArrayList<>();
for (int i = 0; i <= ltop; i++) {
list.add(data[i]);
}
for (int i = rtop; i < data.length; i++) {
list.add(data[i]);
}
it = list.iterator();
}

@Override
public boolean hasNext() {
return it.hasNext();
}

@Override
public E next() {
return it.next();
}
}
}

队列

队列性质

  • FIFO
  • 队列的入队时间复杂度为 O(1) 出队时间复杂度为O(n)

底层数据结构

  • 线性表 ArrayList

手写代码实现

Queue

1
2
3
4
5
6
7
8
9
10
11
12
13
package P1.Interface;

public interface Queue<E> extends Iterable<E>{
//入队
public void offer(E element);
//出队
public E poll();
//查看队首元素
public E element();
public boolean isEmpty();
public void clear();
public int size();
}

ArrayQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package P2.linearStructure;

import P1.Interface.Queue;

import java.util.Iterator;

public class ArrayQueue<E> implements Queue<E> {

private ArrayList<E> list;

public ArrayQueue() {
list = new ArrayList<>();
}

@Override
public void offer(E element) {
list.add(list.size(), element);
}

@Override
public E poll() {
return list.remove(0);
}

@Override
public E element() {
return list.get(0);
}

@Override
public boolean isEmpty() {
return list.isEmpty();
}

@Override
public void clear() {
list.clear();
}

@Override
public int size() {
return list.size();
}

@Override
public Iterator<E> iterator() {
return list.iterator();
}

@Override
public String toString() {
return list.toString();
}

@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (o instanceof ArrayQueue) {
ArrayQueue other = (ArrayQueue) o;
return list.equals(other.list);
}
return false;
}
}

队列的相关应用

  • 队列实现栈
  • 栈实现队列

QueueToStack.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package P2.linearStructure;

import P1.Interface.Stack;

import java.util.Iterator;

//队列实现栈
public class QueueToStack {
public static void main(String[] args) {
StackImplByQueue<Integer> stack = new StackImplByQueue<>();
System.out.println(stack);
for (int i = 1; i <= 5; i++) {
stack.push(i);
}
System.out.println(stack);
System.out.println(stack.peek());
System.out.println(stack.pop());
System.out.println(stack);
}
}

class StackImplByQueue<E> implements Stack<E> {

private ArrayQueue<E> queueA;

public StackImplByQueue() {
queueA = new ArrayQueue<>();
}

@Override
public int size() {
return queueA.size();
}

@Override
public boolean isEmpty() {
return queueA.isEmpty();
}

@Override
public void push(E element) {
queueA.offer(element);
}

@Override
public E pop() {
if (isEmpty()) {
throw new IllegalArgumentException("stack is null");
}
for (int i = 0; i < queueA.size() - 1; i++) {
E tmp = queueA.poll();
queueA.offer(tmp);
}
return queueA.poll();
}

@Override
public E peek() {
if (isEmpty()) {
throw new IllegalArgumentException("stack is null");
}
for (int i = 0; i < queueA.size() - 1; i++) {
E tmp = queueA.poll();
queueA.offer(tmp);
}
E tmp = queueA.poll();
queueA.offer(tmp);
return tmp;
}

@Override
public void clear() {
queueA.clear();
}

@Override
public Iterator<E> iterator() {
return queueA.iterator();
}

@Override
public String toString() {
return queueA.toString();
}
}

StackToQueue.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package P2.linearStructure;

//栈实现队列
import P1.Interface.Queue;
import java.util.Iterator;

/*
理论:
使用两个栈
stackA
stackB
入队操作:
所有入队操作直接进入A
出队操作:
将A中所求元素向后所有元素弹出进入B
取出所求元素
再将B中元素进去A
先进后出 后进先出
逻辑相消 --> 先进先出

*/

public class StackToQueue {
public static void main(String[] args) {
QueueImplByStack<Integer> queue = new QueueImplByStack<>();
for (int i = 1; i <= 5; i++) {
queue.offer(i);
}
System.out.println(queue);
System.out.println(queue.poll());
System.out.println(queue);
}
}

class QueueImplByStack<E> implements Queue<E> {
private ArrayStack<E> stackA;
private ArrayStack<E> stackB;
public QueueImplByStack() {
stackA = new ArrayStack<>();
stackB = new ArrayStack<>();
}

@Override
public void offer(E element) {
stackA.push(element);
}

@Override
public E poll() {
if (isEmpty()) {
throw new IllegalArgumentException("queue is null");
}
while (stackA.size() != 1) {
stackB.push(stackA.pop());
}
E ret = stackA.pop();

while (!stackB.isEmpty()) {
stackA.push(stackB.pop());
}
return ret;
}

@Override
public E element() {
if (isEmpty()) {
throw new IllegalArgumentException("queue is null");
}
while (stackA.size() != 1) {
stackB.push(stackA.pop());
}
E ret = stackA.peek();

while (!stackB.isEmpty()) {
stackA.push(stackB.pop());
}
return ret;
}

@Override
public boolean isEmpty() {
return stackA.isEmpty();
}

@Override
public void clear() {
stackA.clear();
}

@Override
public int size() {
return stackA.size();
}

@Override
public Iterator<E> iterator() {
return stackA.iterator();
}

@Override
public String toString() {
return stackA.toString();
}
}

循环队列

循环队列性质

  • 使用 队首队尾 指针辅助操作
  • 对比 普通队列 入队时间复杂度为 O(1) 出队时间复杂度为O(n) 循环队列的==出队时间复杂度同样为 O(1)==
  • 循环队列本质上的是 出队时 队首元素 被取出 同时 Front 指针直接后移一位

底层数据结构

  • 静态数组

手写代码实现

ArrayLoopQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package P2.linearStructure;

import P1.Interface.Queue;

import java.util.Iterator;

//循环队列
public class ArrayLoopQueue<E> implements Queue<E> {

private E[] data; //存储数据的容器
private int front; //队首指针(实际上就是数组中的索引角标)
private int rear; //队尾指针
private int size; //元素的个数 (f < r r-f ; r < f r+L-f)
private static int DEFAULT_CAPACITY = 10; //默认容量

public ArrayLoopQueue() {
data = (E[]) new Object[DEFAULT_CAPACITY + 1];
front = 0;
rear = 0;
size = 0;
}

@Override
public void offer(E element) {
//满了没
if ((rear + 1) % data.length == front) {
resize(data.length * 2 - 1);
}
data[rear] = element;
rear = (rear + 1) % data.length;
size++;
}

@Override
public E poll() {
//空不空
if (isEmpty()) {
throw new IllegalArgumentException("queue is null");
}
E ret = data[front];
front = (front + 1) % data.length;
size--;
if (size <= (data.length - 1) / 4 && data.length - 1 > DEFAULT_CAPACITY) {
resize(data.length / 2 + 1);
}
return ret;
}

private void resize(int newLen) {
E[] newData = (E[]) new Object[newLen];
int index = 0;
for (int i = front; i != rear; i = (i + 1) % data.length) {
newData[index++] = data[i];
}
data = newData;
front = 0;
rear = index;
}

@Override
public E element() {
if (isEmpty()) {
throw new IllegalArgumentException("queue is null");
}
return data[front];
}

@Override
public boolean isEmpty() {
return front == rear;
}

@Override
public void clear() {
data = (E[]) new Object[DEFAULT_CAPACITY];
size = 0;
front = 0;
rear = 0;
}

@Override
public int size() {
return size;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('[');
if (isEmpty()) {
sb.append(']');
return sb.toString();
}
for (int i = front; i != rear; i = (i + 1) % data.length) {
sb.append(data[i]); //相当于加如data[i] 的toString方法
if ((i + 1) % data.length == rear) {
sb.append(']');
} else {
sb.append(',');
sb.append(' ');
}
}
return sb.toString();
}

@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (o instanceof ArrayLoopQueue) {
ArrayLoopQueue<E> other = (ArrayLoopQueue<E>) o;
if (size != other.size) {
return false;
}
int i = front;
int j = other.front;
while (i != rear) {
if (!data[i].equals(other.data[j])) {
return false;
}
i = (i + 1) % data.length;
j = (j + 1) % other.data.length;
}
return true;
}
return false;
}

@Override
public Iterator<E> iterator() {
return new ArrayLoopQueueIterator();
}

class ArrayLoopQueueIterator implements Iterator<E> {
private int cur = front;

@Override
public boolean hasNext() {
return cur != rear;
}

@Override
public E next() {
E ret = data[cur];
cur = (cur + 1) % data.length;
return ret;
}
}
}

注意

  • 底层实现的循环逻辑 当 Rear 到队尾时 进行循环至队首 解决空间浪费问题
  • 注意 队列满足的判空判满条件同为 (Rear + 1) % n == Front 导致歧义 模糊定义 故而需要定义 Rear 尾指针始终指向null 从而解决歧义问题 此时判满条件为 (Rear + 1) % n == Front 判满条件为 Rear == Front

双端队列

双端队列性质

  • 使用头尾指针辅助操作队列
  • 限定插入和删除操作在表两端进行操作的线性表 –表头和表尾都可以进行插入和删除
  • 同时具有队列和栈性质的一种数据结构
  • 本质上是循环队列的一种升级
  • 插入删除的时间复杂度都是 O(1)

手写代码实现

Dequeue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package P1.Interface;

public interface Dequeue<E> extends Queue<E> {

//表首添加
public void addFirst(E element);
//表尾添加
public void addLast(E element);
//表首删除
public E removeFirst();
//表尾删除
public E reomveLast();
//查看表首
public E getFirst();
//查看表尾
public E getLast();
}

ArrayDequeue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package P2.linearStructure;

import P1.Interface.Dequeue;
import P1.Interface.Stack;

import java.util.Iterator;

public class ArrayDequeue<E> implements Dequeue<E>, Stack<E> {

private E[] data;
private int front;
private int rear;
private int size;
private static int DEFAULT_CAPACITY = 10;

public ArrayDequeue() {
data = (E[]) new Object[DEFAULT_CAPACITY + 1];
front = 0;
rear = 0;
size = 0;
}

@Override
public void addFirst(E element) {
if ((rear + 1) % data.length == front ) {
resize(data.length * 2 - 1);
}
front = (front - 1 + data.length) % data.length;
data[front] = element;
size++;
}

@Override
public void addLast(E element) {
if ((rear + 1) % data.length == front ) {
resize(data.length * 2 - 1);
}
data[rear] = element;
rear = (rear + 1) % data.length;
size++;
}

private void resize(int newLen) {
E[] newData = (E[]) new Object[newLen];
int index = 0;
for (int i = front; i != rear; i = (i + 1) % data.length) {
newData[index++] = data[i];
}
data = newData;
front = 0;
rear = index;
}

@Override
public E removeFirst() {
if (isEmpty()) {
throw new IllegalArgumentException("queue is null");
}
E ret = data[front];
front = (front + 1) % data.length;
size--;
if (size <= (data.length - 1) / 4 && data.length - 1 > DEFAULT_CAPACITY) {
resize(data.length / 2 +1);
}
return ret;
}

@Override
public E reomveLast() {
if (isEmpty()) {
throw new IllegalArgumentException("queue is null");
}
rear = (rear -1 + data.length) % data.length;
E ret = data[rear];
size--;
if (size <= (data.length - 1) / 4 && data.length - 1 > DEFAULT_CAPACITY) {
resize(data.length / 2 +1);
}
return ret;
}

@Override
public E getFirst() {
if (isEmpty()) {
throw new IllegalArgumentException("queue is null");
}
return data[front];
}

@Override
public E getLast() {
if (isEmpty()) {
throw new IllegalArgumentException("queue is null");
}
return data[(rear - 1 + data.length) % data.length];
}

@Override
public void offer(E element) {
addLast(element);
}

@Override
public E poll() {
return removeFirst();
}

@Override
public E element() {
return getFirst();
}

@Override
public E peek() {
return getLast();
}

@Override
public boolean isEmpty() {
return size == 0 && front == rear;
}

@Override
public void push(E element) {
addLast(element);
}

@Override
public E pop() {
return reomveLast();
}

@Override
public void clear() {
E[] data = (E[]) new Object[DEFAULT_CAPACITY];
front = 0;
rear = 0;
size = 0;
}

@Override
public int size() {
return size;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('[');
if (isEmpty()) {
sb.append(']');
return sb.toString();
}
for (int i = front; i != rear; i = (i + 1) % data.length) {
sb.append(data[i]);
if ((i + 1) % data.length == rear) {
sb.append(']');
} else {
sb.append(',');
sb.append(']');
}
}
return sb.toString();
}

@Override
public Iterator iterator() {
return new ArrayDequequeTterator();
}

class ArrayDequequeTterator implements Iterator<E> {

private int cur = front;

@Override
public boolean hasNext() {
return cur != rear;
}

@Override
public E next() {
E ret = data[cur];
cur = (cur + 1) % data.length;
return ret;
}
}
}

链式结构

链表性质

  • 数据域指针域 构成
  • 头节点 区分 为 虚拟头节点真实头节点 头节点的不同 相对应的 头插法 也不同
  • 每个节点存储的数据类型相同
  • 头指针尾指针 仅仅是一个引用变量 是存储头节点与尾节点地址的指针

单向链表

底层数据结构

  • 链表结构
  • 指针 引用地址的应用

核心

  • 定义内部节点内部类 存储 数据域指针域
  • 定义头指针尾指针 –辅助操作链表
  • 增删 –实现按位插入
  • 插入的时间复杂度 O(1) 删除的时间复杂度 O(n)
  • 节点比较的是引用变量存储的地址 而不是内容 所以 节点的比较用 ==
  • 排序 –冒泡排序 或 选择排序 由于 单向链表无法向前获取元素位置

手写代码实现

LinkedSinglyList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
package P3.chainStructure;

//单向链表
import P1.Interface.List;
import java.util.Comparator;
import java.util.Iterator;

public class LinkedSinglyList<E> implements List<E> {

//定义结点类 内部类
private class Node {
//数据域
E data;
//结点域
Node next;

//构造函数
public Node() {
data = null;
next = null;
}
public Node(E data) {
this.data = data;
next = null;
}
public Node(E data, Node next) {
this.data = data;
this.next = next;
}

@Override
public String toString() {
return data.toString();
}
}

private Node head; //头指针
private Node tail; //尾指针
private int size; //链表元素个数 同时元素个数也是结点个数

//构造函数 空链表
public LinkedSinglyList() {
head = null;
tail = null;
size = 0;
}

//构造函数 传入数组 将数组封装成链表
public LinkedSinglyList(E[] arr) {
if (arr == null || arr.length == 0) {
throw new IllegalArgumentException("arr is null");
}
for (int i = 0; i < arr.length; i++) {
add(arr[i]);
}
}

@Override
public void add(E element) {
add(size, element);
}

@Override
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add index out of range");
}
Node n = new Node(element);
if (size == 0) {
head = n;
tail = n;
} else if (index == 0) {
n.next = head;
head = n;
} else if (index == size) {
tail.next = n;
tail = n;
} else {
Node p = head;
for (int i = 0; i < index - 1; i++) {
p = p.next;
}
n.next = p.next;
p.next = n;
}
size++;
}

@Override
public void remove(E element) {
int index = indexOf(element);
if (index != -1) {
remove(index);
}
}

@Override
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("index is out of range");
}
E ret = null;
if (size == 1) {
ret = head.data;
head = null;
tail = null;
} else if (index == 0) {
Node n = head;
ret = n.data;
head = n.next;
n.next = null;
} else if (index == size - 1) {
Node p = head;
while (p.next != tail) {
p = p.next;
}
ret = tail.data;
p.next = null;
tail = p;
} else {
Node p = head;
for (int i = 0; i < index - 1; i++) {
p = p.next;
}
Node n = p.next;
ret = n.data;
p.next = n.next;
n.next = null;
}
size--;
return ret;
}

@Override
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("get index out of range");
}
if (index == 0) {
return head.data;
} else if (index == size - 1) {
return tail.data;
} else {
Node p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}
return p.data;
}
}

@Override
public E set(int index, E element) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("get index out of range");
}
E ret = null;
if (index == 0) {
ret = head.data;;
head.data = element;
} else if (index == size - 1) {
tail.data = element;
} else {
Node p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}
ret = p.data;
p.data = element;
}
return ret;
}

@Override
public int size() {
return size;
}

@Override
public int indexOf(E element) {
if (isEmpty()) {
return -1;
}

Node p = head;
int index = 0;
while (!p.data.equals(element)) {
p = p.next;
index++;
if (p == null) {
return -1;
}
}
return index;
}

@Override
public boolean contains(E element) {
return indexOf(element) != -1;
}

@Override
public boolean isEmpty() {
return size == 0 && head == null && tail == null;
}

@Override
public void clear() {
head = null;
tail = null;
size = 0;
}

@Override
public void sort(Comparator<E> c) {
if (c == null) {
throw new IllegalArgumentException("comparator can not be null");
}

// 插入排序进行 链表的排序
// 此处的出入排序的时间复杂度为O(n^3) 故而用插入排序不合适
// for (int i = 0; i < size; i++) {
// E e = get(i);
// int j = 0;
// for (j = i; j > 0 && c.compare(get(j - 1), e) > 0; j--) {
// set(j, get(j - 1));
// }
// set(j, e);
// }

//故而借助于选择排序来进行排序
//选择 选择排序的原因是因为 选择排序和冒泡排序 的整体数据向后 后移
//时间复杂度为O(n^2)
if (size == 0 || size == 1) {
return;
}
Node nodeA = head;
Node nodeB = nodeA.next;
while (true) {
while (true) {
if (c.compare(nodeA.data, nodeB.data) > 0) {
swap(nodeA, nodeB);
}
if (nodeB == tail) {
break;
}
nodeB = nodeB.next;
}
if (nodeA.next == tail) {
//避免空指针异常
break;
}
nodeA = nodeA.next;
nodeB = nodeA.next;
}
}

private void swap(Node nodeA, Node nodeB) {
E tmp = nodeA.data;
nodeA.data = nodeB.data;
nodeB.data = tmp;
}

@Override
public List<E> subList(int fromIndex, int toIndex) {
//0 <= fromIndex <= toIndex <= size - 1 [fromIndex,toIndex]
if (fromIndex < 0 || toIndex >= size ||fromIndex > toIndex) {
throw new IllegalArgumentException("must 0 <= fromIndex <= toIndex <= size");
}

LinkedSinglyList<E> list = new LinkedSinglyList<>();
// 时间复杂度相当于 O(n^2)
// for (int i = fromIndex; i <= toIndex; i++) {
// list.add(get(i));
// }
// return list;

//整体时间复杂度为O(n)
Node nodeA = head;
for (int i = 0; i < fromIndex; i++) {
nodeA = nodeA.next;
}
Node nodeB = head;
for (int i = 0; i < toIndex; i++) {
nodeB = nodeB.next;
}

Node p = nodeA;
while (true) {
list.add(p.data);
if (p == nodeB) {
break;
}
p = p.next;

}
return list;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('[');
if (isEmpty()) {
sb.append(']');
} else {
Node p = head;
while (true) {
sb.append(p.data);
if (p == tail) {
sb.append(']');
break;
}
sb.append(',');
sb.append(' ');
p = p.next;
}
}
return sb.toString();
}

@Override
public Iterator<E> iterator() {
return new LinkedSinglyListIterator();
}

class LinkedSinglyListIterator implements Iterator<E> {

private Node cur = head;

@Override
public boolean hasNext() {
return cur != null;
}

@Override
public E next() {
E ret = cur.data;
cur = cur.next;
return ret;
}
}
}

单向循环链表

单向循环链表性质

  • 定义头指针尾指针 –辅助操作链表
  • 单向链表头尾不相连 而单向循环链表 尾节点指向头节点

手写代码实现

LinkedSinglyCircularList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
package P3.chainStructure;

import P1.Interface.List;

import java.util.Comparator;
import java.util.Iterator;

//单向循环链表
public class LinkedSinglyCircularList<E> implements List<E> {

//定义结点类 内部类
private class Node {
//数据域
E data;
//结点域
Node next;

//构造函数
public Node() {
data = null;
next = null;
}

public Node(E data) {
this.data = data;
next = null;
}

public Node(E data, Node next) {
this.data = data;
this.next = next;
}

@Override
public String toString() {
return data.toString();
}
}

private Node head; //头指针
private Node tail; //尾指针
private int size; //链表元素个数 同时元素个数也是结点个数

//构造函数 空链表
public LinkedSinglyCircularList() {
head = null;
tail = null;
size = 0;
}

//构造函数 传入数组 将数组封装成链表
public LinkedSinglyCircularList(E[] arr) {
if (arr == null || arr.length == 0) {
throw new IllegalArgumentException("arr is null");
}
for (int i = 0; i < arr.length; i++) {
add(arr[i]);
}
}

@Override
public void add(E element) {
add(size, element);
}

@Override
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add index out of range");
}
Node n = new Node(element);
if (size == 0) {
head = n;
tail = n;
//new code
tail.next = head;
} else if (index == 0) {
n.next = head;
head = n;
// new code
tail.next = head;
} else if (index == size) {
// new code
n.next = tail.next;
tail.next = n;
tail = n;
} else {
Node p = head;
for (int i = 0; i < index - 1; i++) {
p = p.next;
}
n.next = p.next;
p.next = n;
}
size++;
}

@Override
public void remove(E element) {
int index = indexOf(element);
if (index != -1) {
remove(index);
}
}

@Override
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("index is out of range");
}
E ret = null;
if (size == 1) {
ret = head.data;
head = null;
tail = null;
} else if (index == 0) {
Node n = head;
ret = n.data;
head = n.next;
n.next = null;
//new code
tail.next = head;
} else if (index == size - 1) {
Node p = head;
while (p.next != tail) {
p = p.next;
}
ret = tail.data;
//change code
p.next = head;
tail = p;
} else {
Node p = head;
for (int i = 0; i < index - 1; i++) {
p = p.next;
}
Node n = p.next;
ret = n.data;
p.next = n.next;
n.next = null;
}
size--;
return ret;
}

@Override
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("get index out of range");
}
if (index == 0) {
return head.data;
} else if (index == size - 1) {
return tail.data;
} else {
Node p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}
return p.data;
}
}

@Override
public E set(int index, E element) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("get index out of range");
}
E ret = null;
if (index == 0) {
ret = head.data;
;
head.data = element;
} else if (index == size - 1) {
tail.data = element;
} else {
Node p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}
ret = p.data;
p.data = element;
}
return ret;
}

@Override
public int size() {
return size;
}

@Override
public int indexOf(E element) {
Node p = head;
int index = 0;
while (!p.data.equals(element)) {
p = p.next;
index++;
if (p == head) { //change code
return -1;
}
}
return index;
}

@Override
public boolean contains(E element) {
return indexOf(element) != -1;
}

@Override
public boolean isEmpty() {
return size == 0 && head == null && tail == null;
}

@Override
public void clear() {
head = null;
tail = null;
size = 0;
}

@Override
public void sort(Comparator<E> c) {
if (c == null) {
throw new IllegalArgumentException("comparator can not be null");
}

// 插入排序进行 链表的排序
// 此处的出入排序的时间复杂度为O(n^3) 故而用插入排序不合适
// for (int i = 0; i < size; i++) {
// E e = get(i);
// int j = 0;
// for (j = i; j > 0 && c.compare(get(j - 1), e) > 0; j--) {
// set(j, get(j - 1));
// }
// set(j, e);
// }

//故而借助于选择排序来进行排序
//选择 选择排序的原因是因为 选择排序和冒泡排序 的整体数据向后 后移
//时间复杂度为O(n^2)
if (size == 0 || size == 1) {
return;
}
Node nodeA = head;
Node nodeB = nodeA.next;
while (true) {
while (true) {
if (c.compare(nodeA.data, nodeB.data) > 0) {
swap(nodeA, nodeB);
}
if (nodeB == tail) {
break;
}
nodeB = nodeB.next;
}
if (nodeA.next == tail) {
//避免空指针异常
break;
}
nodeA = nodeA.next;
nodeB = nodeA.next;
}

}

private void swap(Node nodeA, Node nodeB) {
E tmp = nodeA.data;
nodeA.data = nodeB.data;
nodeB.data = tmp;
}

@Override
public List<E> subList(int fromIndex, int toIndex) {
//0 <= fromIndex <= toIndex <= size - 1 [fromIndex,toIndex]
if (fromIndex < 0 || toIndex >= size || fromIndex > toIndex) {
throw new IllegalArgumentException("must 0 <= fromIndex <= toIndex <= size");
}

LinkedSinglyList<E> list = new LinkedSinglyList<>();

// 时间复杂度相当于 O(n^2)
// for (int i = fromIndex; i <= toIndex; i++) {
// list.add(get(i));
// }
// return list;

//整体时间复杂度为O(n)
Node nodeA = head;
for (int i = 0; i < fromIndex; i++) {
nodeA = nodeA.next;
}
Node nodeB = head;
for (int i = 0; i < toIndex; i++) {
nodeB = nodeB.next;
}

Node p = nodeA;
while (true) {
list.add(p.data);
if (p == nodeB) {
break;
}
p = p.next;
}
return list;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('[');
if (isEmpty()) {
sb.append(']');
} else {
Node p = head;
while (true) {
sb.append(p.data);
if (p == tail) {
sb.append(']');
break;
}
sb.append(',');
sb.append(' ');
p = p.next;
}
}
return sb.toString();
}

@Override
public Iterator<E> iterator() {
return new LinkedSinglyCircularListIterator();
}

class LinkedSinglyCircularListIterator implements Iterator<E> {

private Node cur = head;
private boolean flag = true; //是否在第一圈

@Override
public boolean hasNext() {
if (isEmpty()) {
return false;
}
return flag;
}

@Override
public E next() {
E ret = cur.data;
cur = cur.next;
if (cur == head) {
flag = false;
}
return ret;
}
}

//约瑟夫环问题
public void josephusLoop() {
if (size <= 2) {
return;
}
Node p = head;
while (size != 2) {
p = p.next;
Node del = p.next;
if (del == head) {
head = del.next;
} else if (del == tail) {
tail = p;
}
p.next = del.next;
del.next = null;
p = p.next;
size--;
}
}

//链表反转
public void reverse() {
if (size == 0 || size == 1) {
return;
}
//创建虚拟头结点
Node dummpyHead = new Node();
Node p = head;
for (int i = 0; i < size; i++) {
Node n = new Node(p.data);
if (dummpyHead.next == null) {
tail = n;
}
n.next = dummpyHead.next;
dummpyHead.next = n;
p = p.next;
}
head = dummpyHead.next;
dummpyHead.next = null;
}

//拉丁方阵问题???
//反转不利用新结点,只使用自己的结点

}

注意

  • Iterator 迭代器 判断是否走了一圈问题private boolean flag = true; // 判断是否在第一圈

单向循环链表相关应用

  • 约瑟夫环问题
  • 逢七过问题

SevenGame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package P3.chainStructure;

//逢七过游戏

/*
输入玩家个数
输入从哪个玩家开始
输入从哪个个数开始
输入一共玩几个数字
打印出每个玩家将要报出的所有数字
*/

import P2.linearStructure.ArrayList;
import com.sun.xml.internal.stream.StaxErrorReporter;

import java.util.Scanner;

public class SevenGame {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println(">>>请输入玩家的个数:");
int playerCount = input.nextInt();
System.out.println(">>>请输入从哪个玩家开始:");
int beginPlayer = input.nextInt();
System.out.println(">>>请输入从哪个数字开始:");
int beginNumber = input.nextInt();
System.out.println(">>>请输入数字的最大值:");
int maxNumber = input.nextInt();

//创建玩家的集合
LinkedSinglyCircularList<ArrayList<String>> list = new LinkedSinglyCircularList<>();
//创建玩家对应的对象ArrayList
for (int i = 0; i < playerCount; i++) {
list.add(new ArrayList<>());
}

//开始玩家的索引
int index = beginPlayer - 1;

//将数字依次分给每个玩家
for (int num = beginNumber; num <= maxNumber; num++) {
// String answer = getAnswer(num);
list.get(index++ % playerCount).add(getAnswer(num));
}

for (int i = 0; i < list.size(); i++ ) {
System.out.println("第" + (i + 1) + "位玩家:" + list.get(i));
}
}

private static String getAnswer(int num) {
if (num % 7 == 0 || (num + "").contains("7")) {
return "过";
}
return num + "";
}
}

双向循环链表

双向循环链表性质

  • 定义头指针尾指针 –辅助操作链表
  • 头尾节点相连 尾节点指向头节点 形成循环
  • 双向传递位置节点 ==每个节点指针域== 有 prenext 两个指针来确定位置

手写代码实现

LinkedList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
package P3.chainStructure;

import P1.Interface.Dequeue;
import P1.Interface.List;
import P1.Interface.Stack;

import java.util.Comparator;
import java.util.Iterator;


//双向循环链表
public class LinkedList<E> implements List<E> , Dequeue<E> , Stack<E> {

@Override
public void addFirst(E element) {
add(0, element);
}

@Override
public void addLast(E element) {
add(size, element);
}

@Override
public E removeFirst() {
return remove(0);
}

@Override
public E reomveLast() {
return remove(size - 1);
}

@Override
public E getFirst() {
return get(0);
}

@Override
public E getLast() {
return get(size - 1);
}

private class Node {
//数据域
E data;
//直接前驱
Node pre;
//直接后继
Node next;
public Node() {
this(null, null, null);
}
public Node(E data) {
this(data, null, null);
}
public Node(E data, Node pre, Node next) {
this.data = data;
this.pre = pre;
this.next = next;
}

@Override
public String toString() {
return data.toString();
}
}

private Node head;
private Node tail;
private int size;

public LinkedList() {
head = null;
tail = null;
size = 0;
}

public LinkedList(E[] arr) {
if (arr == null) {
throw new IllegalArgumentException("arr can not be null");
}
for (E e : arr) {
add(e);
}
}

@Override
public void add(E element) {
add(size, element);
}

@Override
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("add index out of range");
}
Node n = new Node(element);
if (size == 0) {
head = n;
tail = n;
tail.next = head;
head.pre = tail;
} else if (index == 0) {
n.pre = head.pre;
n.next = head;
head.pre = n;
head = n;
tail.next = head;
} else if (index == size) {
n.next = tail.next;
tail.next = n;
n.pre = tail;
tail = n;
head.pre = tail;
} else {
Node p, q;
if (index <= size / 2) {
p = head;
for (int i = 0; i < index - 1; i++) {
p = p.next;
}
q = p.next;
p.next = n;
n.pre = p;
q.pre = n;
n.next = q;
} else {
p = tail;
for (int i = size - 1; i > index; i--) {
p = p.pre;
}
q = p.pre;
q.next = n;
n.pre = q;
n.next = p;
p.pre = n;
}
}
size++;
}

@Override
public void remove(E element) {
int index = indexOf(element);
if (index != -1) {
remove(index);
}
}

@Override
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("remove index out of range");
}
E ret = null;
Node node;
if (size == 1) {
ret = head.data;
head = null;
tail = null;
} else if (index == 0) {
ret = head.data;;
node = head.next;
head.next = null;
node.pre = head.pre;
head.pre = null;
head = node;
tail.next = node;

} else if (index == size - 1) {
ret = tail.data;
node = tail.pre;
tail.pre = null;
node.next = tail.next;
tail.next = null;
tail = node;
head.pre = node;
} else {
Node p, q, r;
if (index <= size / 2) {
p = head;
for (int i = 0; i < index - 1; i++) {
p = p.next;
}
q = p.next;
r = q.next;
ret = q.data;
p.next = r;
r.pre = p;
q.next = null;
q.pre = null;
} else {
p = tail;
for (int i = size - 1; i > index + 1; i--) {
p = p.pre;
}
q = p.pre;
r = q.pre;
ret = q.data;
r.next = p;
p.pre = r;
q.next = null;
q.pre = null;
}
}
size--;
return ret;
}

@Override
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("get index out of range");
}
if (index == 0) {
return head.data;
} else if (index == size - 1) {
return tail.data;
} else {
Node p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}
return p.data;
}
}

@Override
public E set(int index, E element) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("get index out of range");
}
E ret = null;
if (index == 0) {
ret = head.data;
head.data = element;
} else if (index == size - 1) {
ret = tail.data;
tail.data = element;
} else {
Node p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}
ret = p.data;
p.data = element;
}
return ret;
}

@Override
public int size() {
return size;
}

@Override
public int indexOf(E element) {
//判空的操作 在测试TreeSet时发现bug 已修改 2022.2.15 23:37
if (isEmpty()) {
return -1;
}
Node p = head;
int index = 0;
while (!p.data.equals(element)) {
p = p.next;
index++;
if (p == head) {
return -1;
}
}
return index;
}

@Override
public boolean contains(E element) {
return indexOf(element) != -1;
}

@Override
public void offer(E element) {
addLast(element);
}

@Override
public E poll() {
return removeFirst();
}

@Override
public E element() {
return getFirst();
}

@Override
public boolean isEmpty() {
return size == 0 && head == null && head == null;
}

@Override
public void push(E element) {
addLast(element);
}

@Override
public E pop() {
return reomveLast();
}

@Override
public E peek() {
return getLast();
}

@Override
public void clear() {
head = null;
tail = null;
size = 0;
}

@Override
public void sort(Comparator<E> c) {
if (c == null) {
throw new IllegalArgumentException("comparator can not be null");
}
//使用插入排序来做
if (size == 0 || size == 1) {
return;
}
for (Node nodeA = head.next; nodeA != head; nodeA = nodeA.next) {
E e = nodeA.data;
Node nodeB;
Node nodeC;
for (nodeB = nodeA, nodeC = nodeB.pre; nodeC != tail && c.compare(nodeC.data, e) > 0 ; nodeB = nodeB.pre, nodeC = nodeC.pre) {
nodeB.data = nodeC.data;
}
nodeB.data = e;
}

}

@Override
public List<E> subList(int fromIndex, int toIndex) {
if (fromIndex < 0 || toIndex >= size || fromIndex > toIndex) {
throw new IllegalArgumentException("must 0 <= fromIndex <= toIndex <= size");
}
Node nodeA = head;
for (int i = 0; i < fromIndex; i++) {
nodeA = nodeA.next;
}
Node nodeB = head;
for (int i = 0; i < toIndex; i++) {
nodeB = nodeB.next;
}
Node p = nodeA;
LinkedList<E> list = new LinkedList<>();
while (true) {
list.add(p.data);
if (p == nodeB) {
break;
}
p = p.next;
}
return list;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('[');
if (isEmpty()) {
sb.append(']');
} else {
Node p = head;
while (true) {
sb.append(p.data);
if (p == tail) {
sb.append(']');
break;
}
sb.append(',');
sb.append(' ');
p = p.next;
}
}
return sb.toString();
}

@Override
public Iterator<E> iterator() {
return new LinkedListIterator();
}

class LinkedListIterator implements Iterator<E> {

private Node cur = head;
private boolean flag = true; //是否在第一圈

@Override
public boolean hasNext() {
if (isEmpty()) {
return false;
}
return flag;
}

@Override
public E next() {
E ret = cur.data;
cur = cur.next;
if (cur == head) {
flag = false;
}
return ret;
}
}
}

链栈

链队列

非线性结构

集合

Set特点

  • 存储不重复元素的容器
  • 链表实现 LinkedSet 有序
  • 二分搜索树实现 TreeSet 有序 自然排序
  • 哈希表实现 HashSet 无序

Map特点

  • 存储键值对数据 K V
  • 根据键 Key 查询值 Value
  • 链表实现 LinkedMap 有序
  • 二分搜索树实现 TreeMap 有序 键自然排序
  • 哈希表实现 HashMap 无序

LinkedSet

LinkedSet特点

  • 存储不重复元素
  • 链表实现 LinkedSet 且有序

手写代码实现

LinkedSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package P7.TreeStructure;

import P1.Interface.Set;
import P3.chainStructure.LinkedList;

import java.util.Iterator;

//底层由链表实现的集合
public class LinkedSet<E> implements Set<E> {

private LinkedList<E> list;
public LinkedSet() {
list = new LinkedList<>();
}

@Override
public void add(E e) {
//O(n)
if (!list.contains(e)) {
list.add(e);
}
}

@Override
public void remove(E e) {
//O(n)
list.remove(e);
}

@Override
public boolean contains(E e) {
//O(n)
return list.contains(e);
}

@Override
public int size() {
return list.size();
}

@Override
public boolean isEmpty() {
return list.isEmpty();
}

@Override
public String toString() {
return list.toString();
}

@Override
public Iterator<E> iterator() {
return list.iterator();
}
}

TreeSet

TreeSet特点

  • 存储不重复元素
  • 二分搜索树实现 TreeSet 有序 自然排序

手写代码实现

TreeSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package P7.TreeStructure;

import P1.Interface.Set;

import java.util.Iterator;

//底层由二分搜索树BST实现的集合
public class TreeSet<E extends Comparable<E>> implements Set<E> {

private BinarySearchTree<E> bst;

public TreeSet() {
bst = new BinarySearchTree<>();
}

@Override
public void add(E e) {
//O(logn)
bst.add(e);
}

@Override
public void remove(E e) {
//O(logn)
bst.remove(e);
}

@Override
public boolean contains(E e) {
//O(logn)
return bst.contains(e);
}

@Override
public int size() {
return bst.size();
}

@Override
public boolean isEmpty() {
return bst.isEmpty();
}

@Override
public String toString() {
return bst.toString();
}

@Override
public Iterator<E> iterator() {
return bst.iterator();
}
}

TreeMap

TreeMap特点

  • 存储键值对数据 K V
  • 二分搜索树实现 TreeMap 有序 键自然排序

手写代码实现

TreeMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package P7.TreeStructure;

import P1.Interface.List;
import P1.Interface.Map;
import P1.Interface.Set;
import P3.chainStructure.LinkedList;

import java.util.Iterator;

public class TreeMap<K extends Comparable<K>, V> implements Map<K, V>, Iterable<Map.Entry<K, V>> {

private class Node {
public K key; //键
public V value; //值
public Node left;
public Node right;
public Node() {}
public Node(K key, V value) {
this.key = key;
this.value = value;
}

@Override
public String toString() {
return "{" + key + ", " + value + "}";
}
}

private Node root;
private int size; //键值对个数

public TreeMap() {
root = null;
size = 0;
}

//辅助函数 获取指定key所在的结点
//以node为根的二分搜索树中 查找 key 所在的结点 --递归方式
private Node getNode(Node node, K key) {
if (node == null) {
return null;
}
if (key.compareTo(node.key) < 0) {
return getNode(node.left, key);
} else if (key.compareTo(node.key) > 0) {
return getNode(node.right, key);
} else {
return node;
}
}

@Override
public void put(K key, V value) {
root = put(root, key, value);
}

private Node put(Node node, K key, V value) {
if (node == null) {
size++;
return new Node(key, value);
}
if (key.compareTo(node.key) < 0) {
node.left = put(node.left, key, value);
} else if (key.compareTo(node.key) > 0) {
node.right = put(node.right, key, value);
} else {
//如果key已经存在 则put为修改
node.value = value;
}
return node;
}

@Override
public V remove(K key) {
Node delNode = getNode(root, key);
if (delNode != null) {
root = remove(root, key);
return delNode.value;
}
return null;
}

private Node remove(Node node, K key) {
if (node == null) {
return null;
}
if (key.compareTo(node.key) < 0) {
node.left = remove(node.left, key);
return node;
} else if (key.compareTo(node.key) > 0) {
node.right = remove(node.right, key);
return node;
} else {
if (node.left == null) {
Node rightNode = node.right;
node.right =null;
size--;
return rightNode;
}
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}

private Node removeMin(Node node) {
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}

private Node minimum(Node node) {
if (isEmpty()) {
return null;
}
if (node.left == null) {
return node;
}
return minimum(node.left);
}

@Override
public boolean contains(K key) {
return getNode(root, key) != null;
}

@Override
public V get(K key) {
Node node = getNode(root, key);
if (node != null) {
return node.value;
}
return null;
}

@Override
public void set(K key, V value) {
Node node = getNode(root, key);
if (node == null) {
throw new IllegalArgumentException("key-value is not exist");
}
node.value = value;
}

@Override
public int size() {
return size;
}

@Override
public boolean isEmpty() {
return size == 0 && root == null;
}

//获取所有键的Set
@Override
public Set<K> keySet() {
TreeSet<K> set = new TreeSet<>();
inOrderKeySet(root, set);
return set;
}

private void inOrderKeySet(Node node, TreeSet<K> set) {
if (node == null) {
return;
}
inOrderKeySet(node.left, set);
set.add(node.key);
inOrderKeySet(node.right, set);
}

//获取所有值的List
@Override
public List<V> values() {
LinkedList<V> list = new LinkedList<>();
inOrderValues(root, list);
return list;
}

private void inOrderValues(Node node, LinkedList<V> list) {
if (node == null) {
return;
}
inOrderValues(node.left, list);
list.add(node.value);
inOrderValues(node.right, list);
}

//获取键值对
@Override
public Set<Entry<K, V>> entrySet() {
TreeSet<Entry<K, V>> entries = new TreeSet<>();
inOrderEntrys(root, entries);
return entries;
}

private void inOrderEntrys(Node node, TreeSet<Entry<K,V>> entries) {
if (node == null) {
return;
}
inOrderEntrys(node.left, entries);
entries.add(new BSTEntry<>(node.key, node.value));
inOrderEntrys(node.right, entries);
}

//自己实现homework
@Override
public String toString() {
return null;
}

//自己实现homework
@Override
public Iterator<Entry<K, V>> iterator() {
return null;
}

//键值对 对象 进行封装
private class BSTEntry<K extends Comparable<K>, V> implements Entry<K, V> {

private K key;
private V value;

public BSTEntry(K key, V value) {
this.key = key;
this.value = value;
}

@Override
public K getKey() {
return key;
}

@Override
public V getValue() {
return value;
}

@Override
public String toString() {
return key + ":" + value;
}

@Override
public int compareTo(Entry<K, V> o) {
return this.getKey().compareTo(o.getKey()) ;
}
}
}

散列结构

哈希表

HashTable特点

  • 散列表也叫哈希表 根据 K-V 直接访问数据结构。通过关键码值映射到表中位置来访问记录,来加快查找速度。此映射函数叫做散列函数,存放记录的数组叫做散列表
  • 给定表 M,存在函数 f(key),对任意给定关键字值 key,代入函数后能得到包含该关键字记录在表中的地址,此表 M 为哈希 (Hash) 表,函数 f(key) 为哈希函数
  • key通过哈希函数得到的索引分布越均匀越好 –通常我们只关注哈希函数的设计
  • 哈希函数的设计原则遵循
    • 一致性
    • 高效性
    • 均匀性
  • 整型 hash函数设计
    • 小范围正整数直接使用
    • 小范围负整数进行偏移处理 -100 ~ 100 => 0 ~ 200
    • 大规模整数进行取模处理 –有局限 分布不均匀 没有利用所有key 增加了hash冲突 –常见解决方法 模一个素数 本质是数论问题

手写代码实现

HashTable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package P7.TreeStructure;

import P1.Interface.List;
import P1.Interface.Map;
import P1.Interface.Set;
import P3.chainStructure.LinkedList;

public class HashTable<K extends Comparable<K>, V> implements Map<K, V> {

//哈希表要缩容和扩容 但不是简单的×2 ÷2 而是利用素数表
// 哈希表预设容量
private static final int[] capacity = {
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 768433, 1572869, 3145739, 6291469,
12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741
};

//哈希表中每个桶的上限和下限 超过上限需要扩容 小于下限需要缩容 避免极端高瘦或者矮胖的情况
//主要因为每个桶最多有N/M个元素
private static final int upperTol = 10;
private static final int lowerTol = 2;

//素数表中的索引 用来标记当前哈希表的长度 扩容++ 缩容--
private int capacityIndex = 0;

//M当前哈希表中桶的个数
private int M;

//size当前哈希表中元素的个数
private int size;

//桶的定义:平衡树的数组
private AVLTreeMap<K, V>[] hashTable;

public HashTable() {
M = capacity[capacityIndex];
hashTable = new AVLTreeMap[M];
for (int i = 0; i < M; i++) {
hashTable[i] = new AVLTreeMap<>();
}
}

private int hash(K key) {
//获取key的哈希值 纠正正负数 再对M取余 得到要去的桶的索引
return key.hashCode() &0x7fffffff % M;
}

@Override
public void put(K key, V value) {
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
if (map.contains(key)) {
//如果该桶中已经包含该键key所对应的键值对 --修改即可
map.put(key, value);
} else {
//在该桶中新增一个键值对
map.put(key, value);
size++;
//新增元素后考虑扩容
if (size > upperTol * M && capacityIndex + 1 < capacity.length) {
capacityIndex++;
resize(capacity[capacityIndex]);
}
}
}

@Override
public V remove(K key) {
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
V ret = null;
if (map.contains(key)) {
ret = map.remove(key);
size--;
//删除元素后考虑缩容
if (size < lowerTol * M && capacityIndex - 1 >= 0) {
capacityIndex--;
resize(capacity[capacityIndex]);
}
}
return ret;
}

private void resize(int newM) {
AVLTreeMap<K, V>[] newHashTable = new AVLTreeMap[newM];
for (int i = 0; i < newHashTable.length; i++) {
newHashTable[i] = new AVLTreeMap<>();
}
M = newM;
for (int i = 0; i < hashTable.length; i++) {
AVLTreeMap<K, V> map = hashTable[i];
for (K key : map.keySet()) {
newHashTable[hash(key)].put(key, map.get(key));
}
}
hashTable = newHashTable;
}

@Override
public boolean contains(K key) {
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
return map.contains(key);
}

@Override
public V get(K key) {
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
return map.get(key);
}

@Override
public void set(K key, V value) {
int index = hash(key);
AVLTreeMap<K, V> map = hashTable[index];
map.set(key, value);
}

@Override
public int size() {
return size;
}

@Override
public boolean isEmpty() {
return size == 0;
}

@Override
public Set<K> keySet() {
TreeSet<K> set = new TreeSet<>();
for (int i = 0; i < hashTable.length; i++) {
AVLTreeMap<K, V> map = hashTable[i];
for (K key : map.keySet()) {
set.add(key);
}
}
return set;
}

@Override
public List<V> values() {
LinkedList<V> list = new LinkedList<>();
for (int i = 0; i < hashTable.length; i++) {
AVLTreeMap<K, V> map = hashTable[i];
for (V value : map.values()) {
list.add(value);
}
}
return list;
}

@Override
public Set<Entry<K, V>> entrySet() {
TreeSet<Entry<K, V>> set = new TreeSet<>();
for (int i = 0; i < hashTable.length; i++) {
AVLTreeMap<K, V> map = hashTable[i];
for (Entry entry : map.entrySet()) {
set.add(entry);
}
}
return set;
}
}

注意要点

  • 解决hash冲突问题 链地址法 –使用另一种数据结构存储同一hash值的
  • 哈希表是时间与空间之间的平衡,本质上哈希函数设计是非常重要的

树的性质

  • Root
  • SubTree
  • Leaf
  • Level / Depth
  • Degree –节点子树个数 / 树内节点的最大值
  • 树通常通过递归操作

二叉树

二叉树的性质

  • 二叉树的基本形态
    • 空二叉树
    • 只有 Root 节点
    • 根节点 只有左子树 / 只有右子树
    • 二叉树 只有左子树 / 只有右子树 –退化成 链表 左右斜树
    • 同时满足拥有 左右子树
    • 满二叉树 所有分支都右 左右节点 最后一层叶子节点 即满树状态 –满二叉树是一种平衡二叉树
    • 完全二叉树 叶子节点层只能出现在最下面两层 最后一层叶子 从右往左连续排布 –完全二叉树是一种平衡二叉树
  • depth = k 的二叉树 最多有 2^k - 1 个节点
  • 二叉树的第 k 层 最多有 2^(k - 1) 个节点

二叉树的顺序存储结构

  • 顺序存储结构在极端情况 ( 左右斜树 ) 下极其浪费空间 只适合存储平衡树
  • 故而二叉树的存储通常使用 链表存储 –二叉链表

二叉树的遍历

  • 深度优先遍历 DFS
    • 前序遍历
      • DLR
      • 递归实现
      • 迭代实现
    • 中序遍历
      • LDR
    • 后序遍历
      • LRD
  • 广度优先遍历 BFS
    • 层序遍历
      • 使用队列辅助操作

二分搜索树

二分搜索树的性质

  • 任何一个节点都大于其左子树所有节点的值 都小于其右子树所有节点的值
  • 该树中元素必须要具有可比性 同时 不包含重复元素
  • 具备自排序能力

手写代码实现

BinarySearchTree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
package P7.TreeStructure;

import P3.chainStructure.LinkedList;

import java.util.Iterator;

//二分搜索树(BST) 不能存储重复的元素 如何解决? 容易产生极端情况(斜树) 非平衡树
//平衡树 在二分搜索树中 任意一个结点的左子树高度和右子树高度差 <= 1
//红黑树 --平衡二分搜索树
public class BinarySearchTree<E extends Comparable<E>> implements Iterable<E> {

//定义二分搜索树的结点信息
private class Node {
public E e; //数据域
public Node left; //左孩子(当前node结点左子树的根)
public Node right; //右孩子(当前node结点右子树的根)
// public int count = 1; //记录元素出现的次数 默认为1
public Node(E e) {
this.e = e;
left = null;
right = null;
}

@Override
public String toString() {
return e.toString();
}
}

private Node root; //根结点的指针 根指针采用真实根结点 如果二分搜索树的根为空则 root == null
private int size; //二分搜索树中元素的个数(结点的个数)

public BinarySearchTree() {
root = null;
size = 0;
}

//向外提供的添加方法 此方法不能直接递归
public void add(E e) {
/*
//迭代思路
Node node = new Node(e);
if (isEmpty()) {
root = node;
size++;
}
Node cur = root;
while (true) {
//新元素比当前元素大 往右走
if (node.e.compareTo(cur.e) > 0) {
if (cur.right == null) {
cur.right = node;
size++;
break;
} else {
cur = cur.right;
}
//新元素比当前元素小 往左走
} else if (node.e.compareTo(cur.e) < 0) {
if (cur.left == null) {
cur.left = node;
size++;
break;
} else {
cur = cur.left;
}
} else {
break;
}
}*/

//递归思路 --二叉树以递归为主
//以当前node为根结点 在其子树中添加新元素 并返回新树的根
root = add(root, e);

}

//在以node为根的树中 插入元素e 并返回新树的根
private Node add(Node node, E e) {
//从下一层到上一层
if (node == null) {
size++;
return new Node(e);
}
//当前层向下一层
if (e.compareTo(node.e) < 0) {
node.left = add(node.left, e);
} else if (e.compareTo(node.e) > 0) {
node.right = add(node.right, e);
}
//等于的情况 想要重复
/*
*else {
* node.count++;
* }
*
*
* */
//当前层向上一层
return node;
}

public boolean contains(E e) {
/*
//迭代思路
Node cur = root;
while (true) {
if (e.compareTo(cur.e) < 0) {
if (cur.left == null) {
return false;
}
cur = cur.left;
} else if (e.compareTo(cur.e) > 0) {
if (cur.right == null) {
return false;
}
cur = cur.right;
} else {
return true;
}
}*/

//递归思路
return contains(root, e);
}

//查看以node为根的二分搜索树中是否包含元素e
private boolean contains(Node node, E e) {
if (node == null) {
return false;
}
if (e.compareTo(node.e) < 0) {
return contains(node.left, e);
} else if (e.compareTo(node.e) > 0) {
return contains(node.right, e);
} else {
return true;
}
}

//前序遍历 --向外提供的API
public void preOrder() {
preOrder(root);
}

//前序遍历 --封装在内部 --以递归方式实现 --以node为根结点进行前序遍历DLR
private void preOrder(Node node) {
if (node == null) {
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}

//前序遍历 --迭代方式 可直接给外界调用 应用数据结构 --栈结构
public void preOrderNR() {
LinkedList<Node> stack = new LinkedList<>();
if (isEmpty()) {
return;
}
stack.push(root);
while (!stack.isEmpty()) {
Node cur = stack.poll();
System.out.println(cur.e);
if (cur.right != null) {
stack.push(cur.right);
}
if (cur.left != null) {
stack.push(cur.left);
}
}
}

//中序遍历 --向外提供的API
public void inOrder() {
inOrder(root);
}

//中序遍历 --封装在内部 --以递归方式实现 --以node为根结点进行中序遍历LDR
private void inOrder(Node node) {
if (node == null) {
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}

//中序遍历 --迭代方式 可直接给外界调用 应用数据结构 --栈结构
public void inOrderNR() {
LinkedList<Node> stack = new LinkedList<>();
Node p = root;
while (p != null) {
stack.push(p);
p = p.left;
}
while (!stack.isEmpty()) {
Node cur = stack.pop();
System.out.println(cur.e);
if (cur.right != null) {
Node n = cur.right;
while (n != null) {
stack.push(n);
n = n.left;
}
}
}
}

//后续遍历 --向外提供的API
public void postOrder() {
postOrder(root);
}

//后序遍历 --封装在内部 --以递归方式实现 --以node为根结点进行后续遍历LRD
private void postOrder(Node node) {
if (node == null) {
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}

//层序遍历 --迭代方式 --使用数据结构 队列结构
public void levelOrder() {
LinkedList<Node> queue = new LinkedList<>();
if (isEmpty()) {
return;
}
queue.offer(root);
while (!queue.isEmpty()) {
Node cur = queue.poll();
System.out.println(cur.e);
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
}

public int size() {
return size;
}

public boolean isEmpty() {
return root == null && size == 0;
}

//寻找二分搜索树中的最小值
public E minimum() {
if (isEmpty()) {
throw new IllegalArgumentException("bst is null");
}
return minimum(root).e;
}

//以node为根结点 查找最小值所在的结点 --递归
//作业:尝试迭代方式实现
private Node minimum(Node node) {
if (node.left == null) {
return node;
}
return minimum(node.left);
}

//寻找二分搜索树中的最大值
public E maximum() {
if (isEmpty()) {
throw new IllegalArgumentException("bst is null");
}
return maximum(root).e;
}

//以node为根结点 查找最大值所在的结点 --递归
//作业:尝试迭代方式实现
private Node maximum(Node node) {
if (node.right == null) {
return node;
}
return maximum(node.right);
}

//删除最小值 --对外提供API
public E removeMin() {
E ret = minimum();
root = removeMin(root);
return ret;
}

//以node为根 删除其中的最小值结点 返回更新后新树的根
private Node removeMin(Node node) {
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}

//删除最大值 --对外提供API
public E removeMax() {
E ret = maximum();
root = removeMax(root);
return ret;
}

private Node removeMax(Node node) {
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}

//删除指定元素 向外api
public void remove(E e) {
root = remove(root, e);
}

//以node为根 删除元素e 并返回删除后新树的根
private Node remove(Node node, E e) {
if (node == null) {
return null;
}
if (e.compareTo(node.e) < 0) {
node.left = remove(node.left, e);
return node;
} else if (e.compareTo(node.e) > 0) {
node.right = remove(node.right, e);
return node;
} else {
//找到了需要删除的结点
//如果待删除结点的左边为空 则右子树直接上去
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}

//如果待删除结点的右边为空 则左子树直接上去
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}

//左右都不为空 把右子树的最小值作为新树的根返回
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
if (isEmpty()) {
sb.append(']');
} else {
Iterator<E> it = iterator();
for (int i = 0; i < size; i++) {
sb.append(it.next());
if (i == size - 1) {
sb.append(']');
} else {
sb.append(',');
}
}
/*while循环
while (it.hasNext()) {
sb.append(it.next());
sb.append(',');
}
sb.deleteCharAt(sb.length() - 1);
sb.append(']');
*/
}
return sb.toString();
}

@Override
public Iterator<E> iterator() {
return new BinarySearchTreeIterator();
}

private class BinarySearchTreeIterator implements Iterator<E> {

private Iterator<E> it;

public BinarySearchTreeIterator() {
LinkedList<E> list = new LinkedList<>();
LinkedList<Node> stack = new LinkedList<>();
Node p = root;
while (p != null) {
stack.push(p);
p = p.left;
}
while (!stack.isEmpty()) {
Node cur = stack.pop();
list.offer(cur.e);
if (cur.right != null) {
Node n = cur.right;
while (n != null) {
stack.push(n);
n = n.left;
}
}
}
it = list.iterator();
}

@Override
public boolean hasNext() {
return it.hasNext();
}

@Override
public E next() {
return it.next();
}
}
}

TODO

  • Iterator细节理解
  • 子树问题具体实现

AVL平衡树

AVL平衡树特点

  • 二分搜素树性质
  • 自平衡树结构 –任意一节点左子树与右子树的高度差不能超过1
  • 平衡因子 –节点左右子树的高度差
  • 自平衡的四种方式
    • 插入元素在不平衡节点左侧的左侧 –LL旋转
    • 插入元素在不平衡节点右侧的右侧 –RR旋转
    • 插入元素在不平衡节点左侧的右侧 –LR旋转 –即先对当前不平衡节点左子节点进行RR左旋转 再对当前不平衡节点进行LL右旋转
    • 插入元素在不平衡节点右侧的左侧 –RL旋转 –即先对当前不平衡节点右子节点进行LL右旋转 再对当前不平衡节点进行RR左旋转

手写代码实现

AVLTreeMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
package P7.TreeStructure;

import P1.Interface.List;
import P1.Interface.Map;
import P1.Interface.Set;
import P2.linearStructure.ArrayList;
import P3.chainStructure.LinkedList;

// AVL平衡二分搜索树实现的映射
// Ologn
public class AVLTreeMap<K extends Comparable<K>, V> implements Map<K, V> {

private class Node {
public K key; //键
public V value; //值
public int height; //高度
public Node left;
public Node right;

public Node(K key, V value) {
this.key = key;
this.value = value;
left = null;
right = null;
height = 1; //新结点高度默认都是1
}
}

private Node root;
private int size;

public AVLTreeMap() {
root = null;
size = 0;
}

//辅助函数 --获取以node为根的子树中 查找key所在的结点
private Node getNode(Node node, K key) {
if (node == null) {
return null;
}
if (key.compareTo(node.key) < 0) {
return getNode(node.left, key);
} else if (key.compareTo(node.key) > 0) {
return getNode(node.right, key);
} else {
return node;
}
}

//辅助函数 --获取某个结点的高度 如果该结点为空 则高度为0
private int getHeight(Node node) {
if (node == null) {
return 0;
}
return node.height;
}

//辅助函数 --计算某个结点的平衡因子(左右子树的高度差) >0左边高 <0右边高 ==0同高
private int getBalanceFactor(Node node) {
if (node == null) {
return 0;
}
return getHeight(node.left) - getHeight(node.right);
}

//辅助函数 --验证是否是一颗二分搜索树
public boolean isBST() {
ArrayList<K> list = new ArrayList<>();
inOrderKeys(root, list);
for (int i =1; i < list.size(); i++) {
if (list.get(i - 1).compareTo(list.get(i)) > 0) {
return false;
}
}
return true;
}

private void inOrderKeys(Node node, ArrayList<K> list) {
if (node == null) {
return;
}
inOrderKeys(node.left, list);
list.add(node.key);
inOrderKeys(node.right, list);
}

//辅助函数 --验证式是否为一颗平衡树
public boolean isBalanced() {
return isBalanced(root);
}

private boolean isBalanced(Node node) {
if (node == null) {
return true;
}
int balancedFactor = getBalanceFactor(node);
if (Math.abs(balancedFactor) > 1) {
return false;
}
return isBalanced(node.left) && isBalanced(node.right);
}

//左旋转(右侧的右侧RR) 将y结点进行左旋转 并返回新根
private Node leftRotate(Node y) {
Node x = y.right;
Node T3 = x.left;
x.left = y;
y.right = T3;
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}

//右旋转(左侧的左侧LL) 将y结点进行右旋转 并返回新根
private Node rightRotate(Node y) {
Node x = y.left;
Node T3 = x.right;
x.right = y;
y.left = T3;
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}


@Override
public void put(K key, V value) {
root = put(root, key, value);
}

private Node put(Node node, K key, V value) {
if (node == null) {
size++;
return new Node(key, value);
}
if (key.compareTo(node.key) < 0) {
node.left = put(node.left, key, value);
} else if (key.compareTo(node.key) > 0) {
node.right = put(node.right, key, value);
} else {
node.value = value;
return node;
}

//当前结点的高度需要更新
node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;

//判断当前结点是否平衡
int balanceFactor = getBalanceFactor(node);

// > 1 说明 当前结点左侧不平衡 node.left >= 0 左侧的左侧不平衡
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
return rightRotate(node);
}

// > 1 说明 当前结点左侧不平衡 node.left < 0 左侧的右侧不平衡
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
node.left = leftRotate(node.left);
return rightRotate(node);
}

// < -1 说明 当前结点的右侧不平衡 node.right >= 0 右侧的左侧不平衡
if (balanceFactor < -1 && getBalanceFactor(node.right) >= 0) {
node.right = rightRotate(node.right);
return leftRotate(node);
}

// < -1 说明 当前结点的右侧不平衡 node.right < 0 右侧的右侧不平衡
if (balanceFactor < -1 && getBalanceFactor(node.right) < 0) {
return leftRotate(node);
}

return node;
}

@Override
public V remove(K key) {
Node delNode = getNode(root, key);
if (delNode != null) {
root = remove(root, key);
return delNode.value;
}
return null;
}

private Node remove(Node node, K key) {
if (node == null) {
return null;
}
Node retNode = null;
if (key.compareTo(node.key) < 0) {
node.left = remove(node.left, key);
retNode = node;
} else if (key.compareTo(node.key) > 0) {
node.right = remove(node.right, key);
retNode = node;
} else {
//找到了需要删除的结点
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
retNode = rightNode;
} else if (node.right == null) {
Node leftNode = node.left;
node.left = null;
retNode = leftNode;
} else {
Node successor = minimum(node.right);
successor.right = remove(node.right, successor.key);
successor.left = node.left;
node.left = node.right = null;
retNode = successor;
}
}

if (retNode == null) {
return retNode;
}

//更新高度
retNode.height = Math.max(getHeight(retNode.left), getHeight(retNode.right)) + 1;

//获取平衡因子判断是否需要自平衡
int balanceFactor = getBalanceFactor(retNode);

// > 1 说明 当前结点左侧不平衡 node.left >= 0 左侧的左侧不平衡
if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0) {
return rightRotate(retNode);
}

// > 1 说明 当前结点左侧不平衡 node.left < 0 左侧的右侧不平衡
if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
retNode.left = leftRotate(retNode.left);
return rightRotate(retNode);
}

// < -1 说明 当前结点的右侧不平衡 node.right >= 0 右侧的左侧不平衡
if (balanceFactor < -1 && getBalanceFactor(retNode.right) >= 0) {
retNode.right = rightRotate(retNode.right);
return leftRotate(retNode);
}

// < -1 说明 当前结点的右侧不平衡 node.right < 0 右侧的右侧不平衡
if (balanceFactor < -1 && getBalanceFactor(retNode.right) < 0) {
return leftRotate(retNode);
}
return retNode;
}

private Node minimum(Node node) {
if (node.left == null) {
return node;
} else {
return minimum(node.left);
}
}

@Override
public boolean contains(K key) {
return getNode(root, key) != null;
}

@Override
public V get(K key) {
Node node = getNode(root, key);
if (node != null) {
return node.value;
}
return null;
}

@Override
public void set(K key, V value) {
Node node = getNode(root, key);
if (node == null) {
throw new IllegalArgumentException("key-value is not exist");
}
node.value = value;
}

@Override
public int size() {
return size;
}

@Override
public boolean isEmpty() {
return size == 0 && root == null;
}

@Override
public Set<K> keySet() {
TreeSet<K> set = new TreeSet<>();
inOrderKeySet(root, set);
return set;
}

private void inOrderKeySet(Node node, TreeSet<K> set) {
if (node == null) {
return;
}
inOrderKeySet(node.left, set);
set.add(node.key);
inOrderKeySet(node.right, set);
}

@Override
public List<V> values() {
LinkedList<V> list = new LinkedList<>();
inOrderValues(root, list);
return list;
}

private void inOrderValues(Node node, LinkedList<V> list) {
if (node == null) {
return;
}
inOrderValues(node.left, list);
list.add(node.value);
inOrderValues(node.right, list);
}

@Override
public Set<Entry<K, V>> entrySet() {
TreeSet<Entry<K, V>> entries = new TreeSet<>();
inOrderEntrys(root, entries);
return entries;
}

private void inOrderEntrys(Node node, TreeSet<Entry<K,V>> entries) {
if (node == null) {
return;
}
inOrderEntrys(node.left, entries);
entries.add(new BSTEntry<>(node.key, node.value));
inOrderEntrys(node.right, entries);
}

//键值对 对象 进行封装
private class BSTEntry<K extends Comparable<K>, V> implements Entry<K, V> {

private K key;
private V value;

public BSTEntry(K key, V value) {
this.key = key;
this.value = value;
}

@Override
public K getKey() {
return key;
}

@Override
public V getValue() {
return value;
}

@Override
public String toString() {
return key + ":" + value;
}

@Override
public int compareTo(Entry<K, V> o) {
return this.getKey().compareTo(o.getKey()) ;
}
}

//前序遍历 --向外提供的API
public void preOrder() {
preOrder(root);
}

//前序遍历 --封装在内部 --以递归方式实现 --以node为根结点进行前序遍历DLR
private void preOrder(Node node) {
if (node == null) {
return;
}
System.out.println(node.key);
preOrder(node.left);
preOrder(node.right);
}
}

Trie前缀树

Trie前缀树特点

  • Trie前缀树又称字典树、单词查找树,是一种多叉树,专门处理字符串
  • 查询每个条目的时间复杂度,和树中一共多少条目无关,其时间复杂度为O(w),w为所查询单词的长度,大多数单词的长度小于10(优势)
  • 空间换时间的思想

手写代码实现

Trie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package P7.TreeStructure;

/*
* Trie前缀树
* 又称字典树、单词查找树,是一种多叉树,专门处理字符串
* 查询每个条目的时间复杂度,和树中一共多少条目无关
* 其时间复杂度为O(w),w为所查询单词的长度,大多数单词的长度小于10(优势)
* 使用虚拟头结点
* 空间换时间的思想
* */
public class Trie {
private class Node {
public boolean isWord;
public int count;
public AVLTreeMap<Character, Node> childs;
public Node() {
this(false);
}
public Node (boolean isWord) {
this.isWord = isWord;
childs = new AVLTreeMap<>();
count = 0;
}
}

private Node root;
private int size; //添加进来的单词的个数

public Trie() {
root = new Node();
size = 0;
}

public int size() {
return size;
}

public int count(String word) {
Node cur = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (cur.childs.get(c) == null) {
return 0;
}
cur = cur.childs.get(c);
}
if (cur.isWord) {
return cur.count;
} else {
return 0;
}
}

public void add(String word) {
Node cur = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (cur.childs.get(c) == null) {
cur.childs.put(c, new Node());
}
cur = cur.childs.get(c);
}
if (!cur.isWord) {
cur.isWord = true;
size++;
cur.count = 1;
} else {
cur.count++;
}
}

public boolean contains(String word) {
Node cur = root;
for (int i =0; i < word.length(); i++) {
char c = word.charAt(i);
if (cur.childs.get(c) == null) {
return false;
}
cur = cur.childs.get(c);
}
return cur.isWord;
}

public boolean isPrefix(String prefix) {
Node cur = root;
for (int i =0; i < prefix.length(); i++) {
char c = prefix.charAt(i);
if (cur.childs.get(c) == null) {
return false;
}
cur = cur.childs.get(c);
}
return cur.isWord;
}
}

最大堆

MaxHeap特点

  • 最大堆又叫二叉堆,是一颗完全二叉树 –区别满二叉树
  • 堆中某个节点的值总不大于其父节点值 –这种堆定义为最大堆 相应的可以定义最小堆
  • 下层某一元素不一定小于上层的某一元素
  • 完全二叉树可以使用数组定义实现该结构 –研究底层后可得知其线性结构 故底层使用 ArrayList 实现

手写代码实现

MaxHeap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package P7.TreeStructure;

import P2.linearStructure.ArrayList;

import java.util.Iterator;

public class MaxHeap<E extends Comparable<E>> implements Iterable<E> {

//最大堆底层使用线性表实现
private ArrayList<E> data;

public MaxHeap() {
data = new ArrayList<>();
}

//辅助函数 --获取父节点的索引
private int parent(int index) {
if (index == 0) {
throw new IllegalArgumentException("no parent");
}
return (index - 1) / 2;
}

//辅助函数 --获取左孩子的索引
private int leftChild(int index) {
return 2 * index + 1;
}

//辅助函数--获取右孩子的索引
private int rightChild(int index) {
return 2 * index + 2;
}

//元素上浮操作
private void siftUp(int k) {
while (k > 0 && data.get(k).compareTo(data.get(parent(k))) > 0) {
data.swap(k, parent(k));
k = parent(k);
}
}

//元素下沉操作
private void siftDown(int k) {

//如果没有左孩子 则也必然没有右孩子 则不需要下沉
//如果有左孩子 右孩子不一定存在 需要判断右孩子存在性
//如果右孩子存在 取左右两个孩子的最大值和k对应的值比较
//如果右孩子不存在 只需要取左孩子的值与k对应值比较
//如果k对应值比左右两个孩子都大 则不用下沉 否则下沉即可
while (leftChild(k) < data.size()) {
int j = leftChild(k);
if (j + 1 < data.size() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(k);
}
if (data.get(k).compareTo(data.get(j)) < 0) {
data.swap(k, j);
k = j;
} else {
break; //当前k比左右两孩子都大 不用下沉
}
}
}

public int size() {
return data.size();
}

public boolean isEmpty() {
return data.isEmpty();
}

public void clear() {
data.clear();
}

public void add(E e) {
data.add(e);
siftUp(data.size() - 1);
}

public E findMax() {
if (data.isEmpty()) {
throw new IllegalArgumentException("maxheap is empty");
}
return data.get(0);
}

public E findMin() {
if (data.isEmpty()) {
throw new IllegalArgumentException("maxheap is empty");
}
E min = data.get(0);
for (int i = 1; i < data.size(); i++) {
if (data.get(i).compareTo(min) < 0) {
min = data.get(i);
}
}
return min;
}

public E extractMax() {
if (data.isEmpty()) {
throw new IllegalArgumentException("maxheap is empty");
}
E max = findMax();
data.swap(0, data.size() - 1);
data.remove(data.size() - 1);
siftDown(0);
return max;
}

//替换最大值 并返回原先最大值
public E replace(E e) {
E res = findMax();
data.set(0, e);
siftDown(0);
return res;
}

@Override
public Iterator<E> iterator() {
return data.iterator();
}

@Override
public String toString() {
return data.toString();
}
}

优先队列

PriorityQueue特点

  • 普通队列 FIFO LILO。优先队列 出队顺序与入队顺序无关,与优先级相关,本质还是队列的应用
  • 常见具体应用 –任务管理器中动态选择优先级最高的任务执行 塔防游戏中优先攻击对象根据距离 威胁 先后
  • 底层使用最大堆MaxHeap来实现

手写代码实现

PriorityQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package P7.TreeStructure;

import P1.Interface.Queue;

import java.util.Iterator;

//优先队列 --由最大堆实现 --线性表 --数组
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
private MaxHeap<E> heap;

public PriorityQueue() {
heap = new MaxHeap<>();
}

@Override
public void offer(E element) {
heap.add(element);
}

@Override
public E poll() {
return heap.extractMax();
}

@Override
public E element() {
return heap.findMax();
}

@Override
public boolean isEmpty() {
return heap.isEmpty();
}

@Override
public void clear() {
heap.clear();
}

@Override
public int size() {
return heap.size();
}

@Override
public Iterator<E> iterator() {
return heap.iterator();
}

@Override
public String toString() {
return heap.toString();
}
}

2 - 3 树

2 - 3 树特点

  • 2 - 3 树本身属于多叉树分支,作为红黑树、B树的前置结构
  • 2 - 3 树同样是一种 平衡树 二分搜索树,只不过是一种多叉树
  • 2 - 3 树中一个节点可以存放一个元素 –a 两个子结点( < a , > a) 或 两个元素 –a b 三个子结点( < a , > a < b , > b)
  • 同时 2 - 3 树 满足二分搜索树的基本性质 –左小右大 不重复
  • 2 - 3 树 是一种绝对平衡的树 –从根节点到任意叶子节点所经过的节点数是一样的 –所有的叶子节点在最后一层
  • 节点增加时
    • 插入 2 节点会直接增加
    • 插入 3 节点会有假想 4 节点、拆分节点、平衡节点、向上合并等步骤

手写代码实现

Tree23

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package Test02;

import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;

public class Tree23 {
Node root = new Node();

public DataItem find(int value) {
Node node = root;
while (node != null) {
DataItem item = node.findItem(value);
if (item == null) {
node = node.getNextNode(value);
} else {
return item;
}
}
return null;
}

public void insert(DataItem item) {
Node node = root;
while (node != null) {
if (node.isFull()) {
// 当遇到满节点时 进行分裂
split(node);
//重新找应该往左还是右子树查找
node = node.getParent().getNextNode(item.getValue());
} else if (node.isLeaf()) {
//找到叶子了
break;
} else {
//继续往下找
node = node.getNextNode(item.getValue());
}
}
node.insertItem(item);
}

public void split(Node node) {
DataItem item1 = node.remove(1);
DataItem item2 = node.remove(2);
Node child2 = node.disconnect(2);
Node child3 = node.disconnect(3);

Node newRight = new Node();
newRight.insertItem(item2);
newRight.connect(0, child2);
newRight.connect(1, child3);

if (node == root) {
//当根节点满时
root = new Node();
root.insertItem(item1);
root.connect(0, node);
root.connect(1, newRight);
node.setParent(root);
newRight.setParent(root);
} else {
//子节点
int loc = node.getParent().insertItem(item1);
Node parent = node.getParent();
int i;
for (i = node.getParent().getNumItems(); i > loc + 1; i--) {
parent.connect(i, parent.getChild(i - 1));
}
node.getParent().connect(i, newRight);
newRight.setParent(parent);
}
//可以发现插入时 树是向上长高的
}

public void levelTraverse() throws InterruptedException {
if (root == null) {
return;
}
//利用对列进行层序遍历
Queue<Node> que = new ArrayBlockingQueue<Node>(20);
((ArrayBlockingQueue<Node>) que).put(root);
while (!que.isEmpty()) {
Node node = que.poll();
node.displayNode();
if (!node.isLeaf()) {
for (int i = 0; i <= node.getNumItems(); i++) {
((ArrayBlockingQueue<Node>) que).put(node.getChild(i));
}
}
}
}
}

class DataItem {
private int value;

public DataItem(int v) {
value = v;
}

@Override
public String toString() {
return "/" + value;
}

public int getValue() {
return value;
}
}

class Node {
private static final int ORDER = 4;
private int numItems = 0;
private Node parent;
private Node childArray[] = new Node[ORDER];
private DataItem itemArray[] = new DataItem[ORDER - 1];

public DataItem findItem(int value) {
for (DataItem item : itemArray) {
if (item.getValue() == value) {
return item;
}
}
return null;
}

public void displayNode() {
for (int i = 0; i < numItems; i++) {
System.out.print(itemArray[i]);
}
System.out.println();
}

public int getNumItems() {
return numItems;
}

public Node getChild(int index) {
return childArray[index];
}

public DataItem remove(int index) {
DataItem item = itemArray[index];
itemArray[index] = null;
numItems--;
return item;
}

public Node disconnect(int index) {
Node node = childArray[index];
childArray[index] = null;
return node;
}

public void connect(int index, Node node) {
childArray[index] = node;
}

public Node getParent() {
return parent;
}

public void setParent(Node node) {
parent = node;
}

public boolean isFull() {
return numItems == 3;
}

public int insertItem(DataItem item) {
int i = 0;
for (; i < numItems; i++) {
if (item.getValue() < itemArray[i].getValue()) {
for (int j = numItems; j > i; j--) {
itemArray[j] = itemArray[j - 1];
}
break;
} else if (item.getValue() == itemArray[i].getValue()) {
return -1;
}
}
itemArray[i] = item;
numItems++;
//返回插入节点的位置
return i;
}

public Node getNextNode(int value) {
for (int i = 0; i < numItems; i++) {
if (value < itemArray[i].getValue()) {
return childArray[i];
}
}
return childArray[numItems];
}

public boolean isLeaf() {
return childArray[0] == null;
}
}

B树

B+树

红黑树

RBTree特点

  • 红黑树是一个等价于 2-3 树 性质二叉树
  • 红黑树 根节点 规定是 黑色 同时每个 叶子节点(NIL)是 黑色
  • 每个 节点 要么是 黑色,要么是 红色
  • 每个 红色节点 的两个 子节点 一定都是 黑色,不能有两个 红色节点 相连
  • 任意一节点到每个 叶子节点 的路径都包含数量相同的 黑色节点
  • 红黑树是保持 “黑平衡” 的二叉树,严格意义上讲不属于平衡二叉树。故相对于 AVL 而言,插入元素与删除元素性能会更好( 降低了树的高度 )
  • 在 2 - 3 树的基础上 3 节点 使用红黑来表示同一级别 –即用红黑使二叉树能满足多叉树效果
  • 红色节点都是左斜的,红色节点本质是为了标记并列关系,而黑色节点RBTree结构上看是红色节点的父节点,但从本质上 ( 2 - 3 树结构上 ) 将是 与 红节点同为 一个 3 节点的两个元素

手写代码实现

RBTree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
/**
* 1.创建RBTree,定义颜色
* 2.创建RBNode
* 3.辅助方法定义:parentOf(node),isRed(node),isBlack(node),setRed(node),setBlack(node),inOrderPrint();
* 4.左旋方式定义:leftRotate(node);
* 5.右旋方式定义:rightRotate(node);
* 6.公开插入接方法定义:insert(K key, V value);
* 7.内部插入接口方法定义:insert(RBNode node);
* 8.修正插入导致红黑树失衡的方法定义:insertFIxUp(RBNode node);
* 9.测试红黑树正确性
*
*
* @param <K>
* @param <V>
*/
public class RBTree<K extends Comparable<K>, V> {
private static final boolean RED = true; // 红色节点
private static final boolean BLACK = false; // 黑色节点

/**
* 根节点的引用
*/
private RBNode root;

public RBNode getRoot() {
return root;
}

/**
* 获取当前节点的父节点
* @param node
*/
private RBNode parentOf(RBNode node) {
if (node != null) {
return node.parent;
}
return null;
}

/**
* 节点是否为红色
* @param node
*/
private boolean isRed(RBNode node) {
if (node != null) {
return node.color == RED;
}
return false;
}

/**
* 节点是否为黑色
* @param node
*/
private boolean isBlack(RBNode node) {
if (node != null) {
return node.color == BLACK;
}
return false;
}

/**
* 设置节点为红色
* @param node
*/
private void setRed(RBNode node) {
if (node != null) {
node.color = RED;
}
}

/**
* 设置节点为黑色
* @param node
*/
private void setBlack(RBNode node) {
if (node != null) {
node.color = BLACK;
}
}

/**
* 中序打印二叉树
*/
public void inOrderPrint() {
inOrderPrint(this.root);
}

private void inOrderPrint(RBNode node) {
if (node != null) {
inOrderPrint(node.left);
System.out.println("key:" + node.key + ",value:" + node.value);
inOrderPrint(node.right);
}
}

/**
* 公开的插入方法
* @param key
* @param value
*/
public void insert(K key, V value) {
RBNode node = new RBNode();
node.setKey(key);
node.setValue(value);
// 新节点 一定是红色!
node.setColor(RED);
insert(node);
}

private void insert(RBNode node) {
// 1.查找当前node的父节点
RBNode parent = null;
// 从 根节点 开始查找
RBNode x = this.root;

while (x != null) {
parent = x;
// cmp > 0 说明 node.key 大于 x.key
// cmp = 0 说明 node.key 等于 x.key 说明需要进行替换操作
// cmp < 0 说明 node.key 小于 x.key 需要到x的左子树查找
int cmp = node.key.compareTo(x.key);
if (cmp > 0) {
x = x.right;
} else if (cmp == 0) {
x.setValue(node.getValue());
return;
} else {
x = x.left;
}
}
node.parent = parent;

if (parent != null) {
// 判断 node与parent 的key 谁大
// cmp > 0 说明 当前node的key比parent的key大,需要把node放入parent的右子节点
// cmp < 0 说明 当前node的key比parent的key小,需要把node放入parent的左子节点
int cmp = node.key.compareTo(parent.key);
if (cmp > 0) {
parent.right = node;
} else {
parent.left = node;
}
} else {
this.root = node;
}

// 需要调用修复红黑树平衡的方法,insertFixup();
insertFixup(node);
}

/**
* 插入后修复红黑树平衡的方法
* 情景1:红黑树为空树,将根节点染色为黑色
* 情景2:插入节点的key已经存在
* 情景3:插入节点的父节点为黑色,因为所插入的路径,黑色节点没有变化,所以红黑树依然平衡,不需要处理
*
* 情景4:插入节点的父节点为红色(需要处理)
* 情景4.1:叔叔节点存在,并且为红色(父-叔 双红),将爸爸和叔叔染色为红色,将爷爷染色为红色,并且再以爷爷节点为当前节点,进行下一轮处理
* 情景4.2:叔叔节点不存在,或者为黑色,父节点为爷爷节点的左子树
* 情景4.2.1:插入节点为其父节点的左子节点(LL情况),将爸爸染色为黑色,将爷爷染色为红色,然后以爷爷节点右旋,就完成了
* 情景4.2.2:插入节点为其父节点的右子节点(LR情况),
* 以爸爸节点进行一次左旋,得到LL双红的情景(4.2.1),然后指定爸爸节点为当前节点进行下一轮处理
* 情景4.3:叔叔节点不存在,或者为黑色,父节点为爷爷节点的右子树
* 情景4.3.1:插入节点为其父节点的右子节点(RR情况),将爸爸染色为黑色,将爷爷染色为红色,然后以爷爷节点左旋,就完成了
* 情景4.3.2:插入节点为其父节点的左子节点(RL情况),
* 以爸爸节点进行一次右旋,得到RR双红的情景(4.3.1),然后爸爸节点为当前节点进行下一轮处理
*/
private void insertFixup(RBNode node) {
this.root.setColor(BLACK); // 处理情景1,情景2、3不需要处理

RBNode parent = parentOf(node);
RBNode gparent = parentOf(parent);

// 情景4:插入节点的父节点为红色
if (parent != null && isRed(parent)) {
// 如果父节点为红色,那么一定存在爷爷节点,因为根节点不可能是红色

RBNode uncle = null;

if (parent == gparent.left) { // 父节点为爷爷节点的左子树
uncle = gparent.right;

// 情景4.1:叔叔节点存在,并且为红色
if (uncle != null && isRed(uncle)) {
// 将爸爸和叔叔染色为红色,将爷爷染色为红色,并且再以爷爷节点为当前节点,进行下一轮处理
setBlack(parent);
setBlack(uncle);
setRed(gparent);
insertFixup(gparent);
return;
}
// 情景4.2:叔叔节点不存在,或者为黑色
if (uncle == null || isBlack(uncle)) {
// 插入节点为其父节点的左子节点(LL情况),将爸爸染色为黑色,将爷爷染色为红色,然后以爷爷节点右旋,就完成了
if (node == parent.left) {
setBlack(parent);
setRed(gparent);
rightRotate(gparent);
return;
}
// 情景4.2.2:插入节点为其父节点的右子节点(LR情况)
// 以爸爸节点进行一次左旋,得到LL双红的情景(4.2.1),然后指定爸爸节点为当前节点进行下一轮处理
if (node == parent.right) {
leftRotate(parent);
insertFixup(parent);
return;
}
}
} else { // 父节点为爷爷节点的右子树

uncle = gparent.left;

// 情景4.1:叔叔节点存在,并且为红色
if (uncle != null && isRed(uncle)) {
// 将爸爸和叔叔染色为红色,将爷爷染色为红色,并且再以爷爷节点为当前节点,进行下一轮处理
setBlack(parent);
setBlack(uncle);
setRed(gparent);
insertFixup(gparent);
return;
}

// 情景4.3:叔叔节点不存在,或者为黑色,父节点为爷爷节点的右子树
if (uncle == null || isBlack(uncle)) {
// 情景4.3.1:插入节点为其父节点的右子节点(RR情况),将爸爸染色为黑色,将爷爷染色为红色,然后以爷爷节点左旋,就完成了
if (node == parent.right) {
setBlack(parent);
setRed(gparent);
leftRotate(gparent);
return;
}

// 情景4.3.2:插入节点为其父节点的左子节点(RL情况),
// 以爸爸节点进行一次右旋,得到RR双红的情景(4.3.1),然后爸爸节点为当前节点进行下一轮处理
if (node == parent.left) {
rightRotate(parent);
insertFixup(parent);
return;
}
}
}
}
}


/**
* 左旋方法
* 左旋示意图:左旋x节点
* p p
* | |
* x y
* / \ ----> / \
* lx y x ry
* / \ / \
* ly ry lx ly
*
* 1.x的右子节点指向y的左子节点(ly),将y的左子节点的父节点更新为x
* 2.当x的父节点(不为空时),更新y的父节点为x的父节点,并将x的父节点 指定子节点(当前x的位置)为y
* 3.将x的父节点更新为y,将y的左子节点更新为x
*/
private void leftRotate(RBNode x) {
// 1.x的右子节点指向y的左子节点(ly),将y的左子节点的父节点更新为x
RBNode y = x.right;
x.right = y.left;
if (y.left != null) {
y.left.parent = x;
}

// 2.当x的父节点(不为空时),更新y的父节点为x的父节点,并将x的父节点 指定子节点(当前x的位置)为y
if (x.parent != null) {
y.parent = x.parent;
if (x == x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
} else {
// 说明 x是根节点,吃时需要更新 y为根节点的引用引用
this.root = y;
this.root.parent = null;
}

// 3.将x的父节点更新为y,将y的左子节点更新为x
x.parent = y;
y.left = x;
}

/**
* 右旋方法
* 右旋示意图:右旋y节点
* p p
* | |
* y x
* / \ ----> / \
* x ry lx y
* / \ / \
* lx ly ly ry
*
* 1.将y的左子节点指向x的右子节点(ly),将x的右子节点的父节点更新为y
* 2.当y的父节点(不为空时),更新x的父节点为y的父节点,并将y的父节点 指定子节点(当前y的位置)为x
* 3.将y的父节点更新为x,将x的右子节点更新为y
*/
private void rightRotate(RBNode y) {
// 1.将y的左子节点指向x的右子节点(ly),将x的右子节点的父节点更新为y
RBNode x = y.left;
y.left = x.right;
if (x.right != null) {
x.right.parent = y;
}

// 2.当y的父节点(不为空时),更新x的父节点为y的父节点,并将y的父节点 指定子节点(当前y的位置)为x
if (y.parent != null) {
x.parent = y.parent;
if (y == y.parent.left) {
y.parent.left = x;
} else {
y.parent.right = x;
}
} else {
this.root = x;
this.root.parent = null;
}

// 3.将y的父节点更新为x,将x的右子节点更新为y
y.parent = x;
x.right = y;
}

/**
* 红黑树节点,(父亲节点,左节点,右节点,节点颜色,节点值(K,V) )
* @param <K>
* @param <V>
*/
static class RBNode <K extends Comparable<K>, V> {
private RBNode parent; // 父亲节点
private RBNode left; // 左节点
private RBNode right; // 右节点
private boolean color;
private K key;
private V value;

public RBNode() {
}

public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
this.parent = parent;
this.left = left;
this.right = right;
this.color = color;
this.key = key;
this.value = value;
}

public RBNode getParent() {
return parent;
}

public void setParent(RBNode parent) {
this.parent = parent;
}

public RBNode getLeft() {
return left;
}

public void setLeft(RBNode left) {
this.left = left;
}

public RBNode getRight() {
return right;
}

public void setRight(RBNode right) {
this.right = right;
}

public boolean isColor() {
return color;
}

public void setColor(boolean color) {
this.color = color;
}

public K getKey() {
return key;
}

public void setKey(K key) {
this.key = key;
}

public V getValue() {
return value;
}

public void setValue(V value) {
this.value = value;
}
}
}

注意细节

  • 新插入节点一定是 红色节点
  • 新插入节点左旋换色

许久前跟一哥们去他女同学家修电脑,巧的是女同学跟她闺蜜都在。依稀听见闺蜜说,会修电脑有什么厉害的。我在门前淡淡的想,会修电脑确实没什么厉害的,不过修电脑不要钱确实挺厉害的

Algorithm

排序

排序分类

  • 比较类排序 –时间复杂度不能突破 O(nlogn)

    • 冒泡排序
    • 选择排序
    • 插入排序
    • 快速排序
    • 希尔排序
    • 归并排序
    • 堆排序
  • 非比较类排序 –不通过比较来决定元素间次序 可以突破基于比较排序的时间下限达到 O(n) 的时间复杂度 代价是 空间换时间

    • 计数排序

    • 桶排序

    • 基数排序

排序的稳定性

注意 根据数据分布的不同 来选择更为合适的算法 下为运行时间比较

  • 数据分布 完全随机 – Select > Bubble > Insert > Shell > QuickUpper > Merge
  • 数据分布 大致有序 升序 – Select > Bubble > Shell > Merge > Insert > QuickUpper
  • 数据分布 大致有序 降序 – Select > Bubble > Insert > Shell > Merge > QuickUpper
  • 数据分布 大致平稳 – Bubble > Select > Insert > Shell > Merge > QuickUpper

Sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package P5.SortingAlgorithm;

/*
* 算法的执行时间 除了与算法的策略有关 还跟数据分布情况有关
* 数据的分布情况有:
* ms 完全随机 大致有序 大致平稳
* 选择 150 12 70
* 冒泡 120 13 100
* 插入 20 1 10
* 希尔 3 2 3
* 归并 2 0 3
* 单路快 2 2 2
*
*
* 桶排序(非比较类排序)
* 计数排序(非比较类排序)
* 基数排序(非比较类排序)
* */
public abstract class Sort {

public int[] arr;

public Sort(){}
public Sort(int[] arr) {
this.arr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
this.arr[i] = arr[i];
}
}

public abstract void sort();
public void swap(int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}

比较类排序

冒泡排序

冒泡排序性质

  • 重复依次遍历所有数列 两两比较 判定两者的顺序是否满足要求 如果不满足则交换 从而将 最大或最小的 元素 上浮至末端位置 再从剩余未排序的元素中重复操作
  • 时间复杂度 O(n^2)
  • 空间复杂度 O(1)
  • 稳定

BubbleSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package P5.SortingAlgorithm;

import java.util.Arrays;

//冒泡排序 O(n^2) O(1) 稳定
public class BubbleSort extends Sort{
public BubbleSort(int[] arr) {
super(arr);
}

@Override
public void sort() {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(j, j + 1);
}
}
}
// System.out.println(Arrays.toString(arr));
}
}

选择排序

选择排序性质

  • 选择排序将最大或最小的元素找出 排列到序列起始位置 再从剩余未排序的元素中依次找出 最大或最小 的元素 直至排列完整
  • 时间复杂度 O(n^2)
  • 空间复杂度 O(1)
  • 不稳定

SelectSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package P5.SortingAlgorithm;

import java.util.Arrays;

//选择排序 O(n^2) S(1) 不稳定
public class SelectSort extends Sort{

public SelectSort(int[] arr) {
super(arr);
}

@Override
public void sort() {
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
swap(i, j);
}
}

}
// System.out.println(Arrays.toString(arr));
}
}

插入排序

插入排序性质

  • 插入排序通过构建和完善已有的有序数列 同时依次遍历剩余的无序队列 将无序队列中的元素 挨个插入到有序队列中对应位置
  • 时间复杂度 O(n^2)
  • 空间复杂度 O(1)
  • 稳定

InsertSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package P5.SortingAlgorithm;

import java.util.Arrays;

//插入排序 O(n^2) S(1) 稳定
public class InsertSort extends Sort{
public InsertSort(int[] arr) {
super(arr);
}

@Override
public void sort() {
for (int i = 1; i < arr.length; i++) {
int e = arr[i];
int j = 0;
for (j = i; j > 0 && arr[j - 1] > e; j--) {
arr[j] = arr[j - 1];
}
arr[j] = e;
}
// System.out.println(Arrays.toString(arr));
}
}

快速排序

快速排序性质

  • 通过设置关键字 ( 可将数列第一个数字作为关键字 ) 进行第一趟排序 将待排序的数列分成两个独立的部分 其中一部分关键字均比另一部分小 另一部分比关键字大 再进行重复操作 直至关键字左右两部分的 元素只剩一个或没有 最后将 数据整合归并
  • 时间复杂度 O(nlogn) –对于特例 ( 大致升序或降序 –极端子树情况 ) 情况可能导致时间复杂度升至 O(n^2)
  • 空间复杂度 O(1)
  • 不稳定

快速排序分为 当元素个数少时 优化排序算法选择 –希尔排序与快速排序结合

  • 单路快速排序 –一个关键字
  • 通过交换 保持 关键字左右两部分的元素
  • i j 控制的范围 同步增长 从左到右遍历
  • 双路快速排序 –两个关键字
    • 对比单路快排 将等于的情况 合理的划分到两端
    • i 控制的范围从左开始遍历增长 j 控制的范围从右开始遍历增长
  • 三路快速排序 –三个关键字
    • 三路快排将 大于 v 等于 v 与 小于 v 分为三个部分考虑
    • 小于 v 使用 i 从左往右遍历 通过交换 将 等于 排列在 小于后面 而大于 v 使用 j 从右往左 排列交换 总体通过 i 遍历排序

QuickSort01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package P5.SortingAlgorithm;

import java.util.Arrays;

/*
* 单路快排
* 快速排序基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键
* 字均比另一部分的关键字小,比另一部分的关键字大,则可分别对这两部分记录继续进行排序,
* 以达到整个序列有序
* O(nlogn)
* S(1)
* 不稳定
* */
public class QuickSort01 extends Sort{

public QuickSort01(int[] arr) {
super(arr);
}

@Override
public void sort() {
quickSort(0, arr.length - 1);
// System.out.println(Arrays.toString(arr));
}

private void quickSort(int L, int R) {
if (L >= R) {
return;
}
//先对数组进行划分 并返回划分后的中点
int p = partition(L, R);
quickSort(L, p - 1);
quickSort(p + 1, R);
}

private int partition(int L, int R) {

//优化 随机让后面的数字和第一个数字交换
//L = 5 R = 10 [0,5] + 5 = [5,10]
//尽量避免极端情况 --极端子树情况 退化成 O(n)
swap(L, (int)(Math.random() * (R - L + 1) + L));

int v = arr[L];
//arr[l+1 ~ j] < v < arr[j+1 ~ i)
int j = L;
for (int i = L + 1; i <= R; i++) {
if (arr[i] < v) {
swap(j + 1, i);
j++;
}
}
swap(L, j);
return j;
}
}

QuickSort02

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package P5.SortingAlgorithm;

/*
* 双路快排
* 优化使得,当重复出现多次相同数字时,减少递归次数,使得运行时间更接近于nlogn
* */
import java.util.Arrays;

public class QuickSort02 extends Sort{

public QuickSort02(int[] arr) {
super(arr);
}

@Override
public void sort() {
quicSort(0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}

private void quicSort(int L, int R) {
if (L >= R) {
return;
}
//先对数组进行划分 并返回划分后的中点
int p = partition(L, R);
quicSort(L, p - 1);
quicSort(p + 1, R);

}

private int partition(int L, int R) {
//忧患 随机交换第一个数字和后面的一个数字,尽量避免极端情况(升序情况)
swap(L, (int)(Math.random() * (R - L + 1) + L));

int v = arr[L];
int i = L + 1;
int j = R;
while (true) {
while (i <= R && arr[i] < v) {
i++;
}
while (j >= L + 1 && arr[j] > v) {
j--;
}
if (i > j) {
break;
}
swap(i, j);
i++;
j--;
}
swap(L, j);
return j;
}
}

QuickSort03

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package P5.SortingAlgorithm;

import java.util.Arrays;

// 三路快排
public class QuickSort03 extends Sort{
@Override
public void sort() {
quickSort(0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}

private void quickSort(int L, int R) {
if (L >= R) {
return;
}
swap(L, (int) (Math.random() * (R - L + 1) + L));
int v = arr[L];
int lt = L;
int gt = R + 1;
int i = L + 1;
while (i < gt) {
if (arr[i] < v) {
swap(i, lt + 1);
lt++;
i++;
} else if (arr[i] > v) {
swap(i, gt - 1);
gt--;
} else {
i++;
}
}
swap(L, lt);
quickSort(L, lt - 1);
quickSort(gt, R);
}

}

希尔排序

希尔排序性质

  • Shell 发明第一个突破 O(n^2) 级别的排序 是插入排序的一种改进增强 优先比较距离较远的元素 又称为 缩小增量排序
  • 时间复杂度 O(n^1.3) –Master 主定理
  • 空间复杂度 O(1)
  • 不稳定

ShellSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package P5.SortingAlgorithm;

/*
* shell sort 是第一个突破O(n^2)的排序算法,是简单插入排序的改进版。他与擦汗如排序
* 的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序
* O(n^1.3)
* O(1)
* 不稳定
* */

import java.util.Arrays;

public class ShellSort extends Sort {
public ShellSort(int[] arr) {
super(arr);
}

@Override
public void sort() {
int len = arr.length;
// 选择 grep 跨度空间 可以是 len / 2 也可以是 len / 3 也可以是任意值
// grep 跨度空间是多少 遍历 i 就从哪个元素开始
// 比较跨度空间两端的元素 跨度空间依次向前遍历移动 不符合要求就交换 当 i 之前元素遍历结束 i++ 确定下一个跨度空间位置
// 一轮结束 缩小跨度空间 重复操作 直至跨度空间为 1 --即普通插入排序
for (int gap = len / 2; gap > 0; gap = gap / 2) {
for (int i = gap; i < len; i++) {
int e = arr[i];
int j = i;
while (j - gap >= 0 && arr[j - gap] > e) {
arr[j] = arr[j - gap];
j = j - gap;
}
arr[j] = e;
}
}
// System.out.println(Arrays.toString(arr));
}
}

归并排序

归并排序性质

  • 归并排序采用分治法 将已有序的子序列合并 得到完全有序的序列
  • 将两个有序表合并成一个有序表 称为 二路归并
  • 时间复杂度 O(nlogn)
  • 空间复杂度 O(n)

MergeSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package P5.SortingAlgorithm;

/*
* 归并排序
* 归并排序是建立在归并操作上的一种有效的排序操作。该算法采用分治回溯的思想。
* 将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再
* 使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
* O(nlogn)
* S(n)
* 稳定
* */

import java.util.Arrays;

public class MergeSort extends Sort{

public MergeSort(int[] arr) {
super(arr);
}

@Override
public void sort() {
// 归并排序
mergeSort(0, arr.length - 1);
// System.out.println(Arrays.toString(arr));
}

/*
* @Description: 归并排序
* @param L
* @param R
* @return: void
* @Author: Kking
* @Date: 2022/11/9 17:55
*/
private void mergeSort(int L, int R) {
if (L >= R) {
return;
}
int mid = (L + R) / 2;
//递归排序当前层级的左边
mergeSort(L, mid);
//递归排序当前层级的右边
mergeSort(mid + 1, R);

//左边排序完了 且右边排序完了 进行合并
//特殊 如果左边的最大值arr[mid]小于等于右边的最小值arr[mid+1] 则不需要合并
if (arr[mid] > arr[mid + 1]) {
merge(L, mid, R);
}
}

// 合并 排序完后 合并返回完整有序
private void merge(int L, int mid, int R) {
int[] aux = new int[R - L + 1];
//复制当前层级中 原数组的内容给 aux
for (int k = L; k <= R; k++) {
aux[k - L] = arr[k];
}
//对当前层级中 两个子序列部分进行合并
/*
* L ~ mid 0 ~ Len / 2 - 1
* mid+1 ~ R Len / 2 ~ Len - 1
* */
int i = L;
int j = mid + 1;
for (int k = L; k <= R; k++) {
if (i > mid) {
//判断左边完毕
arr[k] = aux[j - L];
j++;
} else if (j > R) {
//判断右边完毕
arr[k] = aux[i - L];
i++;
} else if (aux[i - L] < aux[j - L]) {
arr[k] = aux[i - L];
i++;
} else {
arr[k] = aux[j - L];
j++;
}
}
}
}

注意

  • 归并两个有序数列 分别使用 i j 指针 依次遍历 比较大小后填入新数组即可

堆排序

HeapSort性质

  • 利用堆数据结构设计。堆是一个近似完全二叉树结构。满足性质–子节点的键值或索引总是小于-最大堆(或大于-最小堆)它的父节点。使用数组定义该结构。注意获取孩子节点索引公式。

  • 时间复杂度 Onlogn 空间复杂度 O1

  • 不稳定

HeapSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package P5.SortingAlgorithm;

import java.util.Arrays;

//堆排序 O(nlogn)
public class HeapSort extends Sort {

public HeapSort(int[] arr) {
super(arr);
}

@Override
public void sort() {
//1、对原数组进行最大堆化
heapify(arr);
int len = arr.length;
//2、将每个范围内的最大值放到该范围的后面即可
for (int i = arr.length - 1; i >= 0; i--) {
swap(0, i);
len--;
siftDown(0, len);
}
System.out.println(Arrays.toString(arr));
}

private void heapify(int[] arr) {
//从最后一个元素开始 将每个元素做下沉处理
for (int i = arr.length - 1; i >= 0; i--) {
siftDown(i, arr.length);
}
}

private void siftDown(int k, int len) {
while (leftChild(k) < len) {
int j = leftChild(k);
if (j + 1 < len && arr[j + 1] > arr[j]) {
j = rightChild(k);
}
if (arr[k] < arr[j]) {
swap(k, j);
k = j;
} else {
break;
}
}
}

//辅助函数 --获取左孩子的索引
private int leftChild(int index) {
return 2 * index + 1;
}

//辅助函数--获取右孩子的索引
private int rightChild(int index) {
return 2 * index + 2;
}
}

非比较类排序

计数排序

CountingSort基本思想

  • 非比较类排序,核心在于将输入的数据转化成键存储在额外开辟的数组空间
  • 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数
  • 时间复杂度 On+m
  • 空间复杂度 Om
  • 稳定

CountingSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package P5.SortingAlgorithm;

import java.util.Arrays;

// 计数排序
public class CountingSort extends Sort {

public CountingSort(int[] arr) {
super(arr);
}

@Override
public void sort() {
//1、找最大值和最小值
int max = arr[0];
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}

//2、确定桶的长度 确定 数据->桶索引的转换关系 和 桶索引->数据的转换关系
//创建桶(桶专门用于统计数据出现的次数)
int[] counts = new int[max - min + 1];
//index = num - min
//num = index + min

//3、遍历原数组arr 将每个数据出现的次数统计在桶里
for (int i = 0; i < arr.length; i++) {
counts[arr[i] - min]++;
}

//4、遍历桶 按照从左到右的顺序 将每个数字按照出现的次数 依次回填arr
int k = 0;
for (int index = 0; index < counts.length; index++) {
for (int count = 0; count < counts[index]; count++) {
int num = index + min;
arr[k++] = num;
}
}
System.out.println(Arrays.toString(arr));
}
}

桶排序

BucketSort性质

  • 桶排序是计数排序的升级,利用函数映射,是否高效取决于这个映射函数。原理即,将数据分到有限个数的桶里,每个桶再分别排序
  • 时间复杂度 On+m
  • 空间复杂度 On+m
  • 稳定

BucketSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package P5.SortingAlgorithm;

import P2.linearStructure.ArrayList;

import java.util.Arrays;
import java.util.Comparator;

//桶排序
public class BucketSort extends Sort {
public BucketSort(int[] arr) {
super(arr);
}

@Override
public void sort() {

//1、找到最大值和最小值
int max = arr[0];
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}

//2、确定桶的个数 并创建桶
int bucketNum = (max - min) / arr.length + 1;
ArrayList<Integer> list[] = new ArrayList[bucketNum];
for (int i = 0; i < list.length; i++) {
list[i] = new ArrayList<>();
}

//3、遍历源数据 将数据进行分类处理
for (int i = 0; i < arr.length; i++) {
list[(arr[i] - min) / arr.length].add(arr[i]);
}

//4、对每个桶进行排序
for (int i = 0; i < list.length; i++) {
list[i].sort(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
// System.out.println("第" + (i + 1) + "个桶:" + list[i].toString());
}

//5、将所有桶中的数据依次返回到原数组中
int index = 0; //原数组的角标
for (int i = 0; i < list.length; i++) {
for (int j = 0; j < list[i].size(); j++) {
arr[index++] = list[i].get(j);
}
}
// System.out.println(Arrays.toString(arr));
}
}

基数排序

RadixSort性质

  • 类似于字符串比较 按照低位先排序,后收集,再依次排序高位,直到所有数最高位。但有些属性是有优先级顺序的,可以先按照低优先级排序,再按照高优先级排序,最后高优先级高的在前,高优先级相同,低优先级高的在前

RadixSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package P5.SortingAlgorithm;


import P3.chainStructure.LinkedList;

import java.util.Arrays;

//基数排序 非比较类排序
public class RadixSort extends Sort {

public RadixSort(int[] arr) {
super(arr);
}

@Override
public void sort() {

//1、找分类--确定所需要收集的轮数(最大值的长度)
int radix = getRadix();

//2、创建桶list所有桶的集合 每一个桶是linkedlist 将其当作队列来使用
LinkedList<Integer>[] list = new LinkedList[10];
for (int i = 0; i < list.length; i++) {
list[i] = new LinkedList<>();
}

//3、开始 分类--收集
for (int r = 1; r <= radix; r++) {
for (int i = 0; i < arr.length; i++) {
list[getIndex(arr[i], r)].offer(arr[i]);
}
int index = 0; //遍历arr原数组
//收集的过程
for (int i = 0; i < list.length; i++) {
while (!list[i].isEmpty()) {
arr[index++] = list[i].poll();
}
}
}

System.out.println(Arrays.toString(arr));
}

private int getIndex(int num, int r) {
int ret = 0;
for (int i = 1; i <= r; i++) {
ret = num % 10;
num /= 10;
}
return ret;
}

private int getRadix() {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return (max + "").length();
}
}

枚举

分治回溯

分治回溯的基本思想

  • 分治算法就是将原问题划分成 n 个规模较小,并且结构与原问题相似的子问题,递归的解决这些子问题,然后不断回溯合并其结果,就能得到原问题的解
  • ==回溯思想同样可以用于枚举==,回溯归并结果,本质思想还是==划分小规模判断暴力破解==,将所有可能的结果依次查找出结果,筛选出==符合要求的结果==,==回溯合并==出最终结果 –枚举的最好表达
    优势在于 划分后思维模式简单化 劣势在于代码可读性较差

递归

递归的基本思想

  • 自身调用自身
  • 递归的应用:将问题分解成规模更小的单位 加以判断和选择处理问题使用与原来问题相同解法 –即==分治 分而治之的思想==
  • 递归的经典问题 阶乘问题 汉诺塔问题 斐波那契数列问题….
  • 递归分治回溯思想的一种实例

递归的优劣

  • 递归策略可以使用少量的代码 将一个大型复杂的问题层层转换为一个与原来问题相似的 规模相较很小的问题来求解
  • 递归解决问题 极大的减少了代码量 但是同样也降低了代码的可读性 导致高昂的维护成本

分治回溯思想应用

  • 汉诺塔问题
  • 全排列问题
  • 棋盘覆盖问题
  • 迷宫问题
  • 八皇后问题 n皇后问题
  • 数独问题
  • 二分查找、折半查找
  • 递归遍历文件夹

RecursionDemo07

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package P4.DivideConquerBacktracking;

//hanoi汉诺塔问题
public class RecursionDemo07 {
public static void main(String[] args) {
String x = "X";
String y = "Y";
String z = "Z";
hanoi(3, x, y, z);
}

private static void hanoi(int n, String begin, String mid, String end) {
if (n == 1) {
System.out.println(begin + "->" + end);
} else {
hanoi(n - 1, begin, end, mid);
System.out.println(begin + "->" + end);
hanoi(n - 1, mid, begin, end);
}
}
}

BacktrackinDemo01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package P4.DivideConquerBacktracking;

import java.util.HashSet;

//全排列
public class BacktrackinDemo01 {
public static void main(String[] args) {
String s = "ABC";
char[] arr = s.toCharArray();
// 但凡涉及到去重问题 优先考虑 Set 除非要求实现去重算法
HashSet<String> set = new HashSet<>();
permutation(set, arr, 0, arr.length - 1);
System.out.println(set);
}

private static void permutation(HashSet<String> set, char[] arr, int from, int to) {
if (from == to) {
set.add(String.valueOf(arr)); //[A,B,C] -> "ABC"
} else {
for (int i = from; i <= to; i++) {
swap(arr, i, from);
permutation(set, arr, from + 1, to);
swap(arr, i, from);
}
}
}

private static void swap(char[] arr, int i, int j) {
char tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}

BacktrackinDemo02

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package P4.DivideConquerBacktracking;

import P3.chainStructure.LinkedList;

//迷宫问题,暴力求解,分治回溯,类似于
public class BacktrackinDemo02 {

private static int[][] maze = {
{1, 1, 1, 1, 1, 1, 1, 1, 1},
{0, 0, 1, 0, 0, 0, 1, 1, 1},
{1, 0, 1, 1, 1, 0, 1, 1, 1},
{1, 0, 0, 1, 0, 0, 1, 1, 1},
{1, 1, 0, 1, 1, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 1, 0, 1},
{1, 0, 1, 1, 1, 0, 0, 0, 1},
{1, 1, 0, 0, 0, 0, 1, 0, 0},
{1, 1, 1, 1, 1, 1, 1, 1, 1}
};

//入口信息
private static int entryX = 1;
private static int entryY = 0;
//出口信息
private static int exitX = 7;
private static int exitY = 8;
//路径访问状态表
private static boolean[][] visitied = new boolean[9][9];
//方向的变化量
private static int[][] direction = {
{-1, 0}, {0, 1}, {1, 0}, {0, -1}
};
//存储路径的栈
private static LinkedList<String> stack = new LinkedList<>();


public static void main(String[] args) {
boolean flag = go(entryX, entryY);
if (flag) {
for (String path : stack) {
System.out.println(path);
}
} else {
System.out.println("迷宫不通");
}
}

//以x,y为入口 看是否能够向下找到出口 返回false找不到
private static boolean go(int x, int y) {
stack.push("(" + x + "," + y + ")"); //拼出(1, 0) 进栈
visitied[x][y] = true;
if (x == exitX && y == exitY) {
return true;
}
//考虑四个方向 上 右 下 左
for (int i = 0; i < direction.length; i++) {
int newX = direction[i][0] + x;
int newY = direction[i][1] + y;
if (isInArea(newX, newY) && isRoad(newX, newY) && !visitied[newX][newY]) {
if (go(newX, newY)) {
return true; //某个方向能通,表示此层次x,y能走下去
}
}
}
stack.pop();
return false; //此层次不通,向上返回false
}

private static boolean isRoad(int x, int y) {
return maze[x][y] == 0;
}

private static boolean isInArea(int x, int y) {
return x >= 0 && x < 9 && y >= 0 && y < 9;
}
}

BacktrackinDemo03

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package P4.DivideConquerBacktracking;

//八皇后问题 n皇后问题
public class BacktrackinDemo03 {

private static int count = 0; //记录解的个数
private static final int N = 4; //N皇后 矩阵的尺寸
private static int[][] arr = new int[N][N]; //棋盘数据 0 为空 1 为皇后

public static void main(String[] args) {
queen(0);
}

//递归 解决 row行 皇后问题 如果 row == N 则一个解 就出来了
private static void queen(int row) {
if (row == N) {
count++;
System.out.println("第" + count + "个解;");
printArr();
} else {
//开始遍历当前行的列
for (int col = 0; col < N; col++) {
if (!isDangerous(row, col)) {
//每次放置皇后之前 都先对当前行进行 清空
for (int c = 0; c < N; c++) {
arr[row][c] = 0;
}
arr[row][col] = 1;
queen(row + 1);
}
}
}
}

private static boolean isDangerous(int row, int col) {
//正上
for (int r = row - 1; r >= 0; r--) {
if (arr[r][col] == 1) {
return true;
}
}
//左上
for (int r = row - 1, c = col - 1; r >= 0 && c >= 0; r-- , c--) {
if (arr[r][c] == 1) {
return true;
}
}
//右上
for (int r = row - 1, c = col + 1; r >= 0 && c < N; r-- , c++) {
if (arr[r][c] == 1) {
return true;
}
}
return false;
}

private static void printArr() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
}
}

BacktrackinDemo04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package P4.DivideConquerBacktracking;

//数独问题
/*
每一行、每一列、每一粗线宫(3*3)内数组均为1-9,不重复
以每个格子为小问题单元
每个格子都有一个循环
*/

import java.io.*;

/*
005300000
800000020
070010500
400005300
010070006
003200080
060500009
004000030
000009700
*/
public class BacktrackinDemo04 {
private static int[][] board = new int[9][9];

public static void main(String[] args) throws IOException {
readFile("Sudoku.txt");
solve(0, 0);

}

//求解x-y格子的解 再继续向下递归求解下一个格子
//本质是求出数独的所有解,但是数独本该只有一个解
private static void solve(int row, int col) {
if (row == 9) {
printBoard();
} else {
if (board[row][col] == 0) {
//需要填数字1-9
for (int num = 1; num <= 9; num++) {
if (!isExist(row, col, num)) {
board[row][col] = num; //当前格子已解决
//解决下一个格子
solve(row + (col + 1) / 9, (col + 1) % 9);
}
//如果此处没有解 则必须清零返回
board[row][col] = 0;
}
} else { //已经存在一个已知数字 直接跳过 去解决下一个格子
solve(row + (col + 1) / 9, (col + 1) % 9);
}

}
}

private static boolean isExist(int row, int col, int num) {
//同行
for (int c = 0; c < 9; c++) {
if (board[row][c] == num) {
return true;
}
}

//同列
for (int r = 0; r < 9; r++) {
if (board[r][col] == num) {
return true;
}
}

//同九宫格3*3
int rowMin = 0;
int colMin = 0;

int rowMax = 0;
int colMax = 0;

if (row >= 0 && row <= 2) {
rowMin = 0;
rowMax = 2;
}

if (row >= 3 && row <= 5) {
rowMin = 3;
rowMax = 5;
}

if (row >= 6 && row <= 8) {
rowMin = 6;
rowMax = 8;
}

if (col >= 0 && col <= 2) {
colMin = 0;
colMax = 2;
}

if (col >= 3 && col <= 5) {
colMin = 3;
colMax = 5;
}

if (col >= 6 && col <= 8) {
colMin = 6;
colMax = 8;
}

for (int r = rowMin; r <= rowMax; r++) {
for (int c = colMin; c <= colMax; c++) {
if (board[r][c] == num) {
return true;
}
}
}
return false;
}

private static void readFile(String fileName) throws IOException {
File file = new File(fileName);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line = null;
int row = 0;
while ((line = br.readLine()) != null) {
for (int col = 0; col < 9; col++) {
board[row][col] = Integer.parseInt(line.charAt(col) + "");
}
row++;
}
}

private static void printBoard() {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
}
}

RecursionDemo04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package P4.DivideConquerBacktracking;

//二分查找、折半查找
public class RecursionDemo04 {
public static int count;
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
int index = binarySearch(arr, 0, arr.length - 1, 5);
System.out.println(index);
System.out.println(count);
}

//在数组arr中L-R区间内进行二分搜索查找key的角标
private static int binarySearch(int[] arr, int L, int R, int key) {
count++;
if (L > R) { //如果元素key不存在
return -1;
}
int M = (L + R) / 2;
if (arr[M] == key) {
return M;
}
if (arr[M] < key) {
return binarySearch(arr, M + 1, R, key);
} else {
return binarySearch(arr, L, M - 1, key);
}
}
}

RecursionDemo05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package P4.DivideConquerBacktracking;

import java.io.File;

//递归遍历文件夹
//LRD相当于二叉树的后序遍历
public class RecursionDemo05 {
public static void main(String[] args) {
File dir = new File("C:\\Users\\黎明\\Desktop\\Java数据结构(code)\\Day04\\DS");
traversal(dir);
}

private static void traversal(File dir) {
File[] files = dir.listFiles();
if (files.length == 0) {
return;
}
for (File file : files) {
if (file.isFile()) {
System.out.println(file.getName());
} else {
System.out.println("[" + file.getName() + "]");
traversal(file);
}
}
}
}

RecursionDemo06

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package P4.DivideConquerBacktracking;

import java.util.Scanner;

//棋盘覆盖问题
//ChessBoardCoverage
public class RecursionDemo06 {

private static int BOARD_SIZE = 8;
private static int[][] board = new int[BOARD_SIZE][BOARD_SIZE];
//代表颜色 同一组L骨牌 编号相同
private static int title = 0; //特殊方格的标志

public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println(">>>请输入特殊方格的索引信息:");
// dr dc 指的是特殊方格的索引
int dr = input.nextInt();
int dc = input.nextInt();

chessBoard(0, 0, dr, dc, BOARD_SIZE);
printBoard();
}

private static void printBoard() {
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
System.out.print(board[i][j] + "\t");
}
System.out.println();
}
}

//在size*size的矩阵中 以tr tc为同部分子矩阵的基点 dr dc 是特殊方格的索引 进行填充
private static void chessBoard(int tr, int tc, int dr, int dc, int size) {
if (size == 1) {
return;
}

//该层要填充L型骨牌
int num = ++title;
//该层要继续分为四个部分 每个部分的尺寸为?
int s = size / 2;
//判断当前特殊方格在哪个部分中

//左上
if (dr < tr + s && dc < tc + s) {
chessBoard(tr, tc, dr, dc, s);
} else {
board[tr + s - 1][tc + s - 1] = num;
chessBoard(tr, tc, tr + s - 1, tc + s - 1, s);
}
//右上
if (dr < tr + s && dc >= tc + s) {
chessBoard(tr, tc + s, dr, dc, s);
} else {
board[tr + s - 1][tc + s] = num;
chessBoard(tr, tc + s, tr + s - 1, tc + s, s);
}
//左下
if (dr >= tr + s && dc < tc + s) {
chessBoard(tr + s, tc, dr, dc, s);
} else {
board[tr + s][tc + s - 1] = num;
chessBoard(tr + s, tc, tr + s, tc + s - 1, s);
}
//右下
if (dr >= tr + s && dc >= tc + s) {
chessBoard(tr + s, tc + s, dr, dc, s);
} else {
board[tr + s][tc + s] = num;
chessBoard(tr + s, tc + s, tr + s, tc + s, s);
}
}
}

查找

查找分类

  • 有序表查找
  • 线性表查找
  • 树结构查找
  • 散列查找

有序表查找

二分查找

  • 迭代实现 –循环
  • 递归实现
  • 时间复杂度 O(log2n)

注意:

  • 二分查找中计算中间点:推荐使用 mid = low + 1 / 2 * ( high - low ) 不推荐使用mid = ( low + high ) / 2 尽量的去避免索引越界 ( out of range ) 问题

RecursionDemo04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package P4.DivideConquerBacktracking;

//二分查找、折半查找 --递归实现
public class RecursionDemo04 {
public static int count;
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
int index = binarySearch(arr, 0, arr.length - 1, 5);
System.out.println(index);
System.out.println(count);
}

//在数组arr中L-R区间内进行二分搜索查找key的角标
private static int binarySearch(int[] arr, int L, int R, int key) {
count++;
if (L > R) { //如果元素key不存在
return -1;
}
int M = (L + R) / 2;
if (arr[M] == key) {
return M;
}
if (arr[M] < key) {
return binarySearch(arr, M + 1, R, key);
} else {
return binarySearch(arr, L, M - 1, key);
}
}
}

插值查找

  • 迭代实现 –循环
  • 递归实现
  • 插值查找本质是更改了二分查找的查找点 将本来的二分 改成了 mid = low + (key - a[low]) / (a[high] - a[low]) * (high - low)
  • 时间复杂度 O(logn )
  • 对于表长较大 关键字分布比较均匀的查找表来说 插值查找算法的平均性能比折半查找更好 反之 若表长较短分布不均 那么插值查找的性能可能有待考量

InterpolationSearch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package P6.SearchAlgorithm;

//插值查找
public class InterpolationSearch {
public static void main(String[] args) {
int[] arr = {-12, -10, -6, -2, 0, 2, 6, 8, 12, 14, 18, 22, 25, 26, 30, 33, 38, 40};
int key = -10;
int index = interpolationSearch(arr,0, arr.length - 1,key);
System.out.println("index:" + index);
System.out.println(count);
count = 0;
index = binarySearch(arr,0,arr.length - 1,key);
System.out.println("index:" + index);
System.out.println(count);
}

private static int binarySearch(int[] arr, int min, int max, int key) {
count++;
if (min > max) {
return -1;
}
int mid = (min + max) / 2;
if (arr[mid] == key) {
return mid;
} else if (key < arr[mid]) {
return binarySearch(arr,min,mid - 1,key);
} else {
return binarySearch(arr,mid + 1,max,key);
}
}

private static int count = 0;
private static int interpolationSearch(int[] arr, int low, int high, int key) {
count++;
if (low > high) {
return -1;
}
int mid = low + (int) (1.0 * (key - arr[low]) / (arr[high] - arr[low]) * (high - low));
if (mid < low || mid > high) {
return -1;
}
if (arr[mid] == key) {
return mid;
} else if (key < arr[mid] ) {
return interpolationSearch(arr,low,mid - 1,key);
} else {
return interpolationSearch(arr,mid + 1, high,key);
}
}
}

斐波那契查找

线性表查找

树结构查找

散列查找

搜索

搜索算法

  • 深度优先搜索 DFS
  • 广度优先搜索 BFS

贪心

动态规划

参考文档:

入门保姆级

字符串匹配

字符串匹配算法

  • KMP
  • 前缀树

位运算

注意 jdk底层使用排序 元素数量小于 28 使用插入排序 后快速排序等

引用数据类型使用归并排序

Java

Java基础学习通常视频时长在200h以上

Java基础:

  • 一阶段
    • Java概述
      • Java历史
      • Java特点
      • Java运行机制
      • JDK
      • 转义字符
      • Java开发规范
      • Java API
    • 变量
      • 数据类型
      • 变量使用
      • 数据类型转换
    • 运算符
      • 运算符介绍
      • 算术运算符
      • 关系运算符
      • 逻辑运算符
      • 赋值运算符
      • 三元运算符
      • 优先级
      • 二进制
      • 位运算符
    • 控制结构
      • 顺序
      • 分支
      • 循环
      • break
      • continue
      • return
    • 数组、排序、查找
      • 数组
      • 排序
      • 查找
    • 面向对象编程OOP
      • 类与对象
      • 成员方法
      • 成员方法传参机制
      • overload
      • 可变参数
      • 作用域
      • 构造器
      • this
      • 访问修饰符
      • 封装
      • 继承
      • 多态
      • Super
      • overwrite
      • Object类详解
      • 断点调试
  • 二阶段
    • 面向对象高级
      • 类变量和类方法
      • main方法语法
      • 代码块
      • 单例设计模式
      • final关键字
      • 抽象类
      • 接口
      • 内部类
    • 枚举和注解
      • 自定义类实现枚举
      • enum关键字实现枚举
      • JDK内置的基本注解类型
      • 元注解:对注解进行注解
    • Exception
      • 异常的概念
      • 异常体系图
      • 常见异常
      • 异常处理
      • 自定义异常
      • throw和throws对比
    • 常用类
      • 包装类
      • String
      • StringBuffer
      • StringBuilder
      • Math
      • Date、Calendar、LocalDate…
      • System
      • Arrays
      • BigInteger BigDecimal
    • 集合
      • 集合框架体系
      • Collection
        • List
          • ArrayList
          • LinkedList
          • Vector
        • Set
          • HashSet
          • LinkedHashSet
          • TreeSet
      • Map
        • HashMap
        • Hashtable
        • LinkedHashMap
        • TreeMap
        • Properties
      • Collections
    • 泛型
      • 泛型语法
      • 自定义泛型
        • 泛型类
        • 泛型接口
        • 泛型方法
      • 泛型继承和通配符
    • 线程
      • 线程介绍
      • 线程使用
        • 继承Thread
        • 实现Runnable
      • 线程方法
      • 线程生命周期
      • Synchronized
      • 互斥锁
      • 死锁
    • IO流
      • 文件
        • 概念
        • 常用操作
      • IO流原理及流的分类
      • 输入流
        • InputStream
          • FileInputStream
          • BufferedInputStream
          • ObjectInputStream
        • Reader
          • FileReader
          • BufferedReader
          • InputStreamReader
      • 输出流
        • OutputStream
          • FileOutputStream
          • BufferedOutputStream
          • ObjectOutputStream
        • Writer
          • FileWriter
          • BufferedWriter
          • InputStreamWriter
      • Properties类
  • 三阶段
    • 网络编程
      • 网络基础
      • InetAddress
      • Socket
      • TCP编程
        • 字节流
        • 字符流
      • UDP编程
    • 反射
      • 反射机制
      • 类的加载
      • 反射获取类的结构信息
        • Class
        • Field
        • Method
        • Constructor
        • 访问属性
        • 访问方法
    • Mysql基础(后续Mysql高级 优化 集群 项目)
      • MySQL安装配置
      • 数据库
        • 创建
        • 查看、删除数据库
        • 备份恢复数据库
        • 创建
        • 删除
        • 修改
      • MySQL数据类型
      • CRUD
        • Insert
        • Update
        • Delete
        • Select
          • 单表
          • 多表
      • 函数
        • 统计函数
        • 时间日期
        • 字符串函数
        • 数学函数
        • 控制流程
      • 内连接
      • 外连接
      • 约束
        • not null
        • primary key
        • unique
        • foreign key
        • check
        • 自增长
      • 索引
        • 主键索引
        • 唯一索引(UNIQUE)
        • 普通索引(INDEX)
        • 全文索引
      • 事务
    • JDBC和连接池
      • JDBC概念
      • JDBC快速入门
      • JDBC API
        • PreparedStatement
        • DriverManager
        • Statement
        • ResultSet
      • JDBCUtils
      • 事务
      • 批处理
      • 连接池
        • DataSource
        • DBCP
        • C3P0
        • Proxool
        • BoneCP
        • Druid
      • Apache-DBUtils
      • DAO增删改查-BasicDao
    • 正则表达式
      • 正则表达式入门
      • 正则表达式基本语法
      • 三个常用类
        • Pattern
        • Matcher
        • PatterSyntaxException
      • 分组、捕获、反向引用
      • 元字符
        • 限定符
        • 选择匹配符
        • 分组组合和反向引用符
        • 特殊字符
        • 字符匹配符
        • 定位符
      • 应用实例
    • 新特性
      • Java8 新特性
        • Lambda
        • 函数式接口
        • 接口静态方法
        • 接口默认方法
        • 方法引用
        • 构造器引用
        • stream API
        • 并行流
        • 串行流
        • Optional
        • 新时间日期 API
      • Java11 新特性
        • 代码层面新特性
          • JShell
          • 类型推断
          • 集合增强API
          • Stream加强
          • 新增字符串处理方法
          • Optional加强
          • InputStream增强API
          • 标准Java异步HTTP客户
        • 其他新特性
          • 简化编译运行
          • 支持Unicode 10
          • Epsilon垃圾收集器
          • ZGC
          • JFR
          • 支持Linux容器
          • 支持G1上的并行完全垃圾手机
          • 增加加密算法 代替RC4
          • 最新HTTPS安全协议TLS 1.3
          • 移除和废弃的内容

Java高级:

JavaWEB:

主流框架和项目管理:

分布式 微服务 并行架构:

DevOps(开发运维一体化) 自动化部署管理项目 解决CI/CD:

大数据技术:

项目实战:

大厂高频面试题:

底层源码/内核研究:

编程基础扩展:

泛型

逆变协变

https://blog.csdn.net/qq_53094471/article/details/125239232

https://www.jianshu.com/p/65284a1a2dee

https://www.cnblogs.com/en-heng/p/5041124.html

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

  • f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
  • f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
  • f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

异常

Throwable

  • Error –系统级别错误
  • Exception
    • RuntimeException
    • CompiletimeException –如日期解析
    • else

IO流

IO流用于读取数据,可以是本地数据,也可以是网络数据

IO流分类

流方向

  • 输入流
  • 输出流

操作文件类型

  • 字节流 –操作所有类型文件 图片 文档 音频 视频
  • 字符流 –只能操作纯文本,能用txt打开(md txt…)

字节流

  • ==InputStream==

    • ==FileInputStream==
    • ==ObjectInputStream==
    • ==BufferedInputStream==
    • 解压缩流 解压缩本质就是将 ZipEntry 按层级拷贝到本地另一个文件夹中
  • ==OutputStream==

    • 字节打印流
      • 压缩流 java中的压缩文件实际上都是一个 ZipEntry 对象
  • 序列化流

    • ObjectInputStream
    • ObjectInputStream

字符流

  • Reader

    • FileReader
    • LineNumberReader
    • BufferedReader
  • Writer

    • 字符打印流
  • ConvertStream –转换流用于字符流与字节流之间的桥梁(两个作用 1. 指定字符集读写[Deprecated] 2. 字节流使用字符流中的方法)

    • InputStreamReader

    • OutputStreamWriter

基本使用

  1. 创建字节流对象
  2. 写数据
  3. 释放资源

乱码原因

  1. 读取数据时,未读完整个汉字
  2. 编码和解码的方式不一致

如何不产生乱码(解决乱码问题)

  1. 尽量不使用字节流读取文本文件
  2. 编码解码时使用同一个码表,同一个编码方式

字节码拷贝通常不会出现问题

BOM(Byte Order Mark),字节顺序标记,出现在文本文件头部,Unicode编码标准中用于标识文件是采用哪种格式的编码。

BOM采用UTF-8编码。几乎所有的文本编辑软件都可以显示并编辑UTF-8编码的文件。但是很遗憾,其中很多软件的表现并不理想。

细节部分

java中的char确实使用2Byte空间,它实际使用的是UCS-2 也就是plane 0,只能表述65536个字符
对于超出其范围的其它plane内容,编译器会直接报错!
尽量别用char类型,因为它会导致一些隐蔽的错误。比如,当你在用String时你定义了一个其它plane的字符,你想当然的认为一个char就能盛放String中的一个字符(毕竟char是字符,而String就是描述的char数组),但是你会发现其实这个String的length()是2而不是1,因为它超出了UCS-2,String用两个char的位置(4字节)来表示了这个char,而String本该用一个char的位置来表示它才对

char可以存储汉字是因为char使用的是unicode编码,且仅使用plane0中包含的字符。unicode的plane0占用两个字节,因此char也占两个字节

java中的char是Unicode字符集存储,unicode编码字符集中包含了汉字,所以可以存储中文; java内部其实是使用的UTF-16的编码

BOM编码

bom头,在文档最前面有一个隐藏的字符标记

image-20230115204942827

序列化问题

image-20230116022104585

解决反序列化异常

https://blog.csdn.net/jj89929665/article/details/123280707

jar包

image-20230117015256185 image-20230117015327682 image-20230117015421717

apache基金会

Apache Pig

Hadoop Flink Spark

lucene

image-20230116164116201

异或与 –加密

字节流

字符流

输入流

输出流

节点流

访问文件的

访问数组的

访问管道的

处理流

缓冲流

对象流

转换流

注解

自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
package Java_Basics;

/**
* 自定义注解
*
* @author Dracula
*/
public @interface MyAnno {
String value();

String name() default "linyuxuan";
}

注解原理

.java 文件编译 -> .class 文件 -> 通过 XJad 反编译 -> .java 可以发现 注解本质就是一个接口

image-20230117031649552

元注解

  • @Retention
  • @Target
image-20230117032351547
1
2
3
4
5
6
7
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnno {
String value();

String name() default "linyuxuan";
}

解析注解

image-20230117032752491
1
2
3
4
5
6
7
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnno {
String value();

String aaa() default "linyuxuan";
}
1
2
3
4
5
6
7
8
9
10
11
12
public class demo3_anno {
public static void main(String[] args) throws ParseException {
Class<Student> clazz = Student.class;
if (clazz.isAnnotationPresent(MyAnno.class)) {
MyAnno anno = clazz.getDeclaredAnnotation(MyAnno.class);
System.out.println(anno.value());
System.out.println(anno.aaa());
}

// 解析方法 或 成员变量 上的注解,同样要先获得类对象 -> 通过反射
}
}

模拟@Test

image-20230117034307032

反射

加载类 class 文件的三种方式

读 class

读 包class

读 Object

image-20230117022708726

https://blog.csdn.net/bejsoiv/article/details/115772964

https://blog.csdn.net/yong_19930826/article/details/107470914

https://blog.csdn.net/wy_05689/article/details/123101320

https://blog.csdn.net/APCSZDDXM/article/details/122006599★★★★

sonarLint提示

https://www.cnblogs.com/aaacarrot/p/16669817.html

动态代理

开闭原则

OOP七大原则

  1. 开闭原则

对扩展开放,对修改关闭。最基础,最重要的一个原则。顾名思义,对一个类允许扩展其功能,但是不能影响到现有的功能模块。对提供方开放,对使用方关闭。

  1. 单一职责原则

控制类的粒度大小,将对象解耦、提高其内聚性。一个类做好自己的事情,尽量不要与其他类建立联系。一个类中的方法只做一件事情。

  1. 接口隔离原则

要为各个类建立它们需要的专用接口。不同的类使用同一个接口的部分方法时,应该将每个类对应的方法从接口中抽离出来分别实现。

  1. 依赖倒置原则

要面向接口编程,不要面向实现编程。高层模块不应该依赖底层模块,二者都依赖其抽象。程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

  1. 里氏替换原则

继承必须确保超类所拥有的性质在子类中仍然成立。超类能出现的地方,可以用子类代替,子类拥有超类的所有属性和方法,通俗来说子类拥有超类的功能,可以拓展超类的功能,但是不能修改超类的功能

  1. 迪米特法则

也叫最少知道原则。只与你的直接朋友交谈,不跟“陌生人”说话。两个对象之间有耦合关系,说明对象间是朋友关系。出现在成员变量,方法参数,方法返回值中的类为直接朋友;出现在局部变量中的类就是陌生朋友

  1. 合成复用原则

尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。判断组合和聚合关系的一个简单方法就是:看整体与部分的生命周期是否一致,部分能够独立于整体而存在则属于聚合;整体消失部分不复存在则属于组合。

静态代理

通过接口传递能力

被代理类提供方法(具体执行方法能力) -> 代理类(new 一个被代理对象 -> 执行需要代理的方法) -> 通过接口来实现两个类之间方法传递

-> 使用代理类执行整个业务(包括代理新增功能 与 被代理类功能)

通过类之间互相方法的创建来组合实现静态代理 -> 每个对象都要创建出来

代理的优势

  • 使真实角色更加纯粹,而不用去关心一些公共业务(业务分工)
  • 当公共业务发生改变或扩展时,可以集中管理公共业务,降低维护成本

缺点

  • 一个真实角色就需要一个代理类,会使得代码的过于臃肿,同时耦合性较高

=> 反射 动态的创建对象

动态代理

动态代理分类

  • 基于API接口 –jdk动态代理
  • 基于类 cglib
  • Java字节码实现 JAVAssist

动态代理的本质 -> 通过 interface + reflect -> 动态的创建对象(反射创建对象的三种方式)来实现 interface 的方法(具备某种能力) -> 来实现某个完整的业务

通过 接口 来实现能力的传递

image-20230120174144870 image-20230120174716371 image-20230120174104091 image-20230120180942166 image-20230120174338136

思考

  1. Java提供了哪些API创建代理?
  2. newProxyInstance 方法在创建代理时,需要接收几个参数,每个参数的含义是什么?
  3. 通过 invokeHandler 的 invoke 方法指定代理执行方法时,invoke 实际的执行过程,需要接收哪几个参数?

JSON

JSON(JavaScript Object Notation, JS对象简谱)是一种轻量级的数据交换格式。它基于 ECMAScript(European Computer Manufacturers Association, 欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率

示范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"firstName": "Brett",
"lastName": "McLaughlin"
}


{
"people": [
{
"firstName": "Brett",
"lastName": "McLaughlin"
},
{
"firstName": "Jason",
"lastName": "Hunter"
}
]
}

JSON对比XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<country>
<name>中国</name>
<province>
<name>黑龙江</name>
<cities>
<city>哈尔滨</city>
<city>大庆</city>
</cities>
</province>
<province>
<name>广东</name>
<cities>
<city>广州</city>
<city>深圳</city>
<city>珠海</city>
</cities>
</province>
<province>
<name>台湾</name>
<cities>
<city>台北</city>
<city>高雄</city>
</cities>
</province>
<province>
<name>新疆</name>
<cities>
<city>乌鲁木齐</city>
</cities>
</province>
</country>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"name": "中国",
"province": [{
"name": "黑龙江",
"cities": {
"city": ["哈尔滨", "大庆"]
}
}, {
"name": "广东",
"cities": {
"city": ["广州", "深圳", "珠海"]
}
}, {
"name": "台湾",
"cities": {
"city": ["台北", "高雄"]
}
}, {
"name": "新疆",
"cities": {
"city": ["乌鲁木齐"]
}
}]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "xdr630",
"favorite": "programming"
}

{
"name":"兮动人",
"age":22,
"fruits":["apple","pear","grape"]
}

{
"string":"\\ \" "
}

{
"number": 1e3,
"n1": 1e2,
"n2": -100
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
1,
2,
"three",
"four",
true,
false,
null,
[
1,
2
],
{
"name": "兮动人"
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<script>
var str = '{"name": "兮动人","age":22}';
var obj = JSON.parse(str);
console.log(obj);
</script>

<script>
var str = '{"name": "兮动人","age":22}';
var obj = JSON.parse(str);
console.log(obj);

var jsonstr = JSON.stringify(obj);
console.log(jsonstr);
</script>

<script>
var str = "console.log('hello')";
eval(str);
</script>

<script>
var str = '{"name":"兮动人","age":22}';
var obj = eval("("+str+")");
console.log(obj)
</script>

<script>
var str = '{"name":"兮动人","age":22}';
var obj = JSON.parse(str)
console.log(obj)
</script>

<script>
var str = '{"name":"兮动人","age":22}';
var obj = JSON.parse(str,fun);
function fun(name,value){
console.log(name+":"+value);
return value
}
console.log(obj)
</script>

<script>
var str = '{"name":"兮动人","age":22}';
var obj = JSON.parse(str,fun);
function fun(name,value){
if (name == "age")
value = 14;
return value
}
console.log(obj)
</script>

<script>
var obj = JSON.stringify(value[, replacer[, space]])
</script>

<script>
var obj = {
a: 1,
b: 2,
c: 3,
d: 4
};
console.log(obj);
var jsonstr = JSON.stringify(obj,["a","b","c"]);
console.log(jsonstr)
var jsonstr = JSON.stringify(obj,["c","a","b"]);
var jsonstr = JSON.stringify(obj,["c","a","b"]);
</script>

<script>
var obj = {
name: "兮动人",
age: 22
}
console.log(obj);
var jsonstr = JSON.stringify(obj);
console.log(jsonstr)
</script>

<script>
var obj = {
name: "兮动人",
age: 22,
a: undefined,
f: function () {

},
b:[function () {}]
}
console.log(obj);
var jsonstr = JSON.stringify(obj);
console.log(jsonstr)
</script>

<script>
var obj = {
a: 1,
b: 2,
c: 3,
d: 4
};
console.log(obj);
var jsonstr = JSON.stringify(obj,["c","a","b"],"one");
console.log(jsonstr)
</script>

<script>
var obj = {
a: 1,
b: 2,
c: 3,
d: 4
};
console.log(obj);
var jsonstr = JSON.stringify(obj,["c","a","b"],"\t");
console.log(jsonstr)
</script>

1
2
3
4
5
6
7
<root>
<name>兮动人</name>
<age>22</age>
<fruits>apple</fruits>
<fruits>pear</fruits>
<fruits>grape</fruits>
</root>

参考文档

JSON与JS操作

Tips

  1. JDK JRE JVM 的包含关系

    • JDK = JRE + 开发工具集(例如javac,java编译工具等)
    • JRE = JVM + Java SE标准类库(Java核心类库)
    • 只运行开发好的.class文件 只需要JRE
  2. DOS命令Disk Operating System 接受指令-解析指令-执行指令

  3. 转义字符

  4. javadoc文档注释

  5. BS架构

    • Java写跨平台项目
    • .net不跨平台项目
    • php门户网站

    Java 原生 jsp + servlet

    ssm + ssh

  6. 从 JDK 5.0 开始, Java 增加了对元数据(MetaData) 的支持, 也就是Annotation(注解) JDK1.5版本

    Java注解基础注解是JDK1.5版本开始引入的一个特性,用于对程序代码的说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。”

  7. Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理。通过使用 Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署

  8. 未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,现在的Struts2有一部分也是基于注解的了,注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式

    注解都是 @ 符号开头的,例如我们在学习方法重写时使用过的 @Override 注解。同 Class 和 Interface 一样,注解也属于一种类型。

    无论是哪一种注解,本质上都一种数据类型,是一种接口类型。到 Java 8 为止 Java SE 提供了 11 个内置注解。其中有 5 个是基本注解,它们来自于 java.lang 包。有 6 个是元注解,它们来自于 java.lang.annotation 包,自定义注解会用到元注解。

    提示:元注解就是负责注解其他的注解。

    基本注解包括:@Override、@Deprecated、@SuppressWarnings、@SafeVarargs 和 @FunctionalInterface。

    常见的Annotation示例

    使用 Annotation 时要在其前面增加 @ 符号, 并把该 Annotation 当成一个修饰符使用。用于修饰它支持的程序元素

    生成文档相关的注解
    @author 标明开发该类模块的作者,多个作者之间使用,分割
    @version 标明该类模块的版本
    @see 参考转向,也就是相关主题
    @since 从哪个版本开始增加的
    @param 对方法中某参数的说明,如果没有参数就不能写
    @return 对方法返回值的说明,如果方法的返回值类型是void就不能写
    @exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
    其中
    @param @return 和 @exception 这三个标记都是只用于方法的。
    @param的格式要求:@param 形参名 形参类型 形参说明
    @return 的格式要求:@return 返回值类型 返回值说明
    @exception的格式要求:@exception 异常类型 异常说明
    @param和@exception可以并列多个

    JDK内置的基本注解
    @Override 注解是用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素。它可以强制一个子类必须重写父类方法或者实现接口的方法。
    @Deprecated 可以用来注解类、接口、成员方法和成员变量等,用于表示某个元素(类、方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择。当其他程序使用已过时的元素时,编译器将会给出警告。
    @SuppressWarnings 注解指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告,且会一直作用于该程序元素的所有子元素。例如,使@SuppressWarnings 修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消显示另一个编译器警告,那么该方法将会同时取消显示这两个编译器警告。

    跟踪代码依赖性,实现替代配置文件功能
    Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @WebServlet("/login")
    public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws
    ServletException, IOException { }
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws
    ServletException, IOException {
    doGet(request, response);
    } }
    1
    2
    3
    4
    5
    6
    7
    8
    <servlet> 
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/login</url-pattern>
    </servlet-mapping>

    自定义Annotation
    定义新的 Annotation 类型使用 @interface 关键字
    自定义注解自动继承了java.lang.annotation.Annotation接口
    Annotation 的成员变量在 Annotation 定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数。类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。  可以在定义 Annotation 的成员变量时为其指定初始值, 指定成员变量的初始值可使用 default 关键字
    如果只有一个参数成员,建议使用参数名为value
    如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为value 可以省略“value=”
    没有成员定义的 Annotation 称为标记; 包含成员变量的 Annotation 称为元数据 Annotation
    注意:自定义注解必须配上注解的信息处理流程才有意义。

    声明自定义注解使用 @interface 关键字(interface 关键字前加 @ 符号)实现。定义注解与定义接口非常像,如下代码可定义一个简单形式的注解类型。

    JDK 中的元注解
    JDK 的元 Annotation 用于修饰其他 Annotation 定义
    JDK5.0提供了4个标准的meta-annotation类型,分别是:

    Retention
    Target
    Documented
    Inherited
    Java 8 又增加了 @Repeatable 和 @Native 两个注解。这些注解都可以在 java.lang.annotation 包中找到。下面主要介绍每个元注解的作用及使用。

    1、@Retention
    @Retention: 只能用于修饰一个 Annotation 定义, 用于指定该 Annotation 的生命周期, @Rentention 包含一个 RetentionPolicy 类型的成员变量, 使用@Rentention 时必须为该 value 成员变量指定值:

    RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释
    RetentionPolicy.CLASS:在class文件中有效(即class保留) , 当运行 Java 程序时, JVM 不会保留注解。 这是默认值
    RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java 程序时, JVM 会保留注释。程序可以通过反射获取该注释。

    2、@Target

    @Target: 用于修饰 Annotation 定义, 用于指定被修饰的 Annotation 能用于修饰哪些程序元素。 @Target 也包含一个名为 value 的成员变量。

    JDK1.8之后,关于元注解@Target的参数类型ElementType枚举值多了两个:
    TYPE_PARAMETER,TYPE_USE。

    在Java 8之前,注解只能是在声明的地方所使用,Java8开始,注解可以应用在任何地方。 

    • ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)。
    • ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中。

1. 集合

1.1 数组

1
2
3
4
Char[] cs = new Char[]{'K', 'k', 'i', 'n', 'g'};
Char[] cs1 = new Char[5];
cs1[0] = 'K';
....

特点:

  1. 内存地址连续 使用之前必须要指定数组长度
  2. 可以通过下标方式访问成员 查询效率高
  3. 增删操作会给系统带来性能消耗 –需要保证避免下标越界的问题 故而动态扩容

1.2 链表

单向链表 与 双向链表

双向链表

特点:

  1. 灵活的空间要求 存储空间不要求连续
  2. 不支持下标访问 支持顺序遍历检索
  3. 针对增删效率会更高 只与操作节点的前后节点有关系 无需移动元素

LinkedList –> 双向链表的一个简单应用

1
2
3
4
5
6
7
8
9
10
11
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

1.3 树

1.3.1 二叉树

二叉树特点:

  1. 某节点的左子树节点值仅包含小于该节点值
  2. 某节点的右子树节点值仅包含大于该节点值
  3. 左右子树每个也必须是二叉查找树
  4. 顺序排列

1.3.2 不平衡二叉树

不平衡二叉树特点:

查询效率不高,面对这个问题我们可以参考在高中学习生物时学过一个关键字去除顶端优势,通过去除植物顶端优势,侧芽会迅速生长,慢慢变得强壮和平衡, 红黑树其实就是去除二叉查找树顶端优势的解决方案,从而达到树的平衡

1.3.3 平衡二叉树

1.3.4 红黑树

RBT(red black tree) –自平衡的二叉树 不是绝对平衡的二叉查找树

树上的节点满足

  1. 每个节点 非黑即红
  2. 根节点必须是黑色
  3. 每个叶子节点【NIL】都是黑色
  4. 每个红色节点的两个子节点必须是黑色 –相邻两层不能同时出现红色 一个红色叶子节点的子结点和父节点必须都是黑色
  5. 任意节点到每个叶子节点的路径包含相同数量的黑节点

红黑树是一个黑平衡二叉树

红黑树遵守规则:

  1. recolor –重新标志节点为红色或者黑色
  2. rotation –选择实现树平衡的关键

红黑树能自平衡 靠的是

  • 左旋:

    以某个节点作为支点(旋转节点),其右子节点变为旋转节点的父节点,右子节点的左子节点变为旋转节点的右子节点,左子节点保持不变

  • 右旋:

    以某个节点作为支点(旋转节点),其左子节点变为旋转节点的父节点,左子节点的右子节点变为旋转节点的左子节点,右子节点保持不变

  • 变色:

    节点的颜色由红变黑或由黑变红

红黑树插入场景

Git

1. Git概述

版本控制

版本控制(Revision control)是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史,方便查看更改历史记录,备份以便恢复以前的版本的软件工程技术。

  • 实现跨区域多人协同开发
  • 追踪和记载一个或者多个文件的历史记录
  • 组织和保护你的源代码和文档
  • 统计工作量
  • 并行开发、提高开发效率
  • 跟踪记录整个软件的开发过程
  • 减轻开发人员的负担,节省时间,同时降低人为错误

简单说就是用于管理多人协同开发项目的技术。

没有进行版本控制或者版本控制本身缺乏正确的流程管理,在软件开发过程中将会引入很多问题,如软件代码的一致性、软件内容的冗余、软件过程的事物性、软件开发过程中的并发性、软件源代码的安全性,以及软件的整合等问题。

无论是工作还是学习,或者是自己做笔记,都经历过这样一个阶段!我们就迫切需要一个版本控制工具!

多人开发就必须要使用版本控制

通常公司会使用 Git SVN 或者 自己搭建 GitLab 来解决版本控制问题

image-20221116221213573

常见版本控制工具

主流的版本控制器有如下这些:

  • Git
  • SVN(Subversion)
  • CVS(Concurrent Versions System)
  • VSS(Micorosoft Visual SourceSafe)
  • TFS(Team Foundation Server)
  • Visual Studio Online

版本控制产品非常的多(Perforce、Rational ClearCase、RCS(GNU Revision Control System)、Serena Dimention、SVK、BitKeeper、Monotone、Bazaar、Mercurial、SourceGear Vault),现在影响力最大且使用最广泛的是Git与SVN

版本控制分类

1、本地版本控制

记录文件每次的更新,可以对每个版本做一个快照,或是记录补丁文件,适合个人用,如RCS。

image-20221116221450626

2、集中版本控制 SVN

所有的版本数据都保存在服务器上,协同开发者从服务器上同步更新或上传自己的修改

image-20221116221749679

所有的版本数据都存在服务器上,用户的本地只有自己以前所同步的版本,如果不连网的话,用户就看不到历史版本,也无法切换版本验证问题,或在不同分支工作。而且,所有数据都保存在单一的服务器上,有很大的风险这个服务器会损坏,这样就会丢失所有的数据,当然可以定期备份。代表产品:SVN、CVS、VSS

3、分布式版本控制 Git

每个人都拥有全部的代码!安全隐患!

所有版本信息仓库全部同步到本地的每个用户,这样就可以在本地查看所有版本历史,可以离线在本地提交,只需在连网时push到相应的服务器或其他用户那里。由于每个用户那里保存的都是所有的版本数据,只要有一个用户的设备没有问题就可以恢复所有的数据,但这增加了本地存储空间的占用。

不会因为服务器损坏或者网络问题,造成不能工作的情况!

image-20221116221842884

Git与SVN的主要区别

SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而工作的时候,用的都是自己的电脑,所以首先要从中央服务器得到最新的版本,然后工作,完成工作后,需要把自己做完的活推送到中央服务器。集中式版本控制系统是必须联网才能工作,对网络带宽要求较高。

image-20221116222008894

Git是分布式版本控制系统,没有中央服务器,每个人的电脑就是一个完整的版本库,工作的时候不需要联网了,因为版本都在自己电脑上。协同的方法是这样的:比如说自己在电脑上改了文件A,其他人也在电脑上改了文件A,这时,你们两之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。Git可以直接看到更新了哪些代码和文件

Git是目前世界上最先进的分布式版本控制系统

Git的历史

同生活中的许多伟大事物一样,Git 诞生于一个极富纷争大举创新的年代。

Linux 内核开源项目有着为数众广的参与者。绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)。到 2002 年,整个项目组开始启用一个专有的分布式版本控制系统 BitKeeper 来管理和维护代码。

Linux社区中存在很多的大佬!破解研究 BitKeeper !逆向反破解

到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux 内核社区免费使用 BitKeeper 的权力。这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds)基于使用 BitKeeper 时的经验教训,开发出自己的版本系统。(2周左右!) 也就是后来的 Git!

Git是目前世界上最先进的分布式版本控制系统。

Git是免费、开源的,最初Git是为辅助 Linux 内核开发的,来替代 BitKeeper!

image-20221116222156709

Linux和Git之父李纳斯·托沃兹(Linus Benedic Torvalds)1969、芬兰

2. Git安装

Git官网下载特别慢 https://git-scm.com/

所有东西下载慢的话就可以去找镜像!

官网下载太慢,我们可以使用淘宝镜像下载:http://npm.taobao.org/mirrors/git-for-windows/

image-20221116224431262

Git官网上有对应版本

windows版本

linux系统包管理器中也有 Git安装包 在linux中 使用 yum 源即可安装

Git安装与卸载

卸载

  1. 反安装
  2. 清理环境变量

安装

无脑下一步

环境变量只是为了全局使用

使用 msi 等 安装包一键式安装都会自动配置环境变量

3. Git基本操作

Git Bash

Git GUI –基本不使用

Git Bash 支持常用 Linux 命令

Git配置

所有的配置文件,其实都保存在本地

1
2
3
4
5
6
7
8
9
10
# git 配置清单
git config -l
# 显示系统配置
git config --system --list
# 显示本地全局配置
git config --global --list
# 注意 用户名 与 密码 是一定要配置的
git config --global user.name "kuangshen" #名称
git config --global user.email 24736743@qq.com #邮箱

当你安装Git后首先要做的事情是设置你的用户名称和e-mail地址。这是非常重要的,因为每次Git提交都会使用该信息。它被永远的嵌入到了你的提交中

只需要做一次这个设置,如果你传递了–global 选项,因为Git将总是会使用该信息来处理你在系统中所做的一切操作。如果你希望在一个特定的项目中使用不同的名称或e-mail地址,你可以在该项目中运行该命令而不要–global选项。总之–global为全局配置,不加为某个项目的特定配置

Git相关的配置目录与文件:

  1. Git\etc\gitconfig :Git 安装目录下的 gitconfig –system 系统级
  2. C:\Users\Administrator\ .gitconfig 只适用于当前登录用户的配置 –global 全局
  3. mingw64 –Git的执行目录

4. Git基本理论

==CORE== 所有 Git 实操都基于 Git 理论基础

工作区域

Git本地有三个工作区域:

  • 工作目录(Working Directory)
  • 暂存区(Stage/Index) –并不存在 本质是一个文件
  • 资源库(Repository或Git Directory)

如果在加上远程的git仓库(Remote Directory)就可以分为四个工作区域

  • 远程的git仓库(Remote Directory)

文件在这四个区域之间的转换关系如下:

详解:

  • Workspace:工作区,就是你平时存放项目代码的地方 –本地项目目录 .idea
  • Index / Stage:暂存区,用于临时存放你的改动,事实上它只是一个文件,保存即将提交到文件列表信息
  • Repository:仓库区(或本地仓库),就是安全存放数据的位置,这里面有你提交到所有版本的数据。其中HEAD指向最新放入仓库的版本 本质上是存放在 .git 目录中 使用文件约束 .git 目录为隐藏文件夹
  • Remote:远程仓库,托管代码的服务器,可以简单的认为是你项目组中的一台电脑用于远程数据交换

本地的三个区域确切的说应该是git仓库中HEAD指向的版本:

image-20221116232143548

文件详解

  • Directory:使用Git管理的一个目录,也就是一个仓库,包含我们的工作空间和Git的管理空间。
  • WorkSpace:需要通过Git进行版本控制的目录和文件,这些目录和文件组成了工作空间。
  • .git:存放Git管理信息的目录,初始化仓库的时候自动创建。
  • Index/Stage:暂存区,或者叫待提交更新区,在提交进入repo之前,我们可以把所有的更新放在暂存区。
  • Local Repo:本地仓库,一个存放在本地的版本库;HEAD会只是当前的开发分支(branch)。
  • Stash:隐藏,是一个工作状态保存栈,用于保存/恢复WorkSpace中的临时状态。
image-20221116233748372
1
2
# HEAD 指向主分支
reg: refs/heads/master
1
2
3
4
5
6
git push
git commit
git add files
git pull
git reset
git checkout

gti commit 后

Git工作流程

git的工作流程

  1. 在工作目录中添加、修改文件;UserMapper.xml
  2. 将需要进行版本管理的文件放入暂存区域;git add
  3. 将暂存区域的文件提交到git仓库。git commit

因此,git管理的文件有三种状态:已修改(modified)已暂存(staged)已提交(committed)

image-20221116232436687

5. Git实操

Git实操

搭建项目

  • 本地新建项目
  • 从远程 clone 项目
image-20221116232859780

工作目录(WorkSpace)一般就是你希望Git帮助你管理的文件夹,可以是你项目的目录,也可以是一个空目录,建议不要有中文。日常使用只要记住图中6个命令

本地仓库搭建

  1. 选择项目目录
  2. 执行命令
1
2
3
git init
Initialized empty Git repository in E:/tmp/Git-test/.git/

clone远程仓库

  1. 选择项目目录
  2. 执行命令
1
git clone https://gitee.com/kuangstudy/kuang_livenote.git

Git文件操作

文件的4种状态

版本控制就是对文件的版本控制,要对文件进行修改、提交等操作,首先要知道文件当前在什么状态,不然可能会提交了现在还不想提交的文件,或者要提交的文件没提交上

  • Untracked: 未跟踪,此文件在文件夹中,但并没有加入到git库,不参与版本控制。通过 git add 状态变为Staged
  • Unmodify: 文件已经入库,未修改,即版本库中的文件快照内容与文件夹中完全一致。这种类型的文件有两种去处,如果它被修改,而变为Modified。如果使用 git rm 移出版本库,则成为Untracked文件
  • Modified: 文件已修改,仅仅是修改,并没有进行其他的操作。这个文件也有两个去处,通过 git add 可进入暂存staged状态,使用 git checkout 则丢弃修改过,返回到unmodify状态,这个 git checkout 即从库中取出文件,覆盖当前修改
  • Staged: 暂存状态。执行 git commit 则将修改同步到库中,这时库中的文件和本地文件又变为一致,文件为Unmodify状态。执行 git reset HEAD filename 取消暂存,文件状态为Modified

查看文件状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查看当前项目文件跟踪
git status

On branch master

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)
kuang_livenote/
project-frontend/

nothing added to commit but untracked files present (use "git add" to track)

# 将当前目录下文件提交到暂存区
git add .

# 提交暂存区内容到本地仓库 -m 提交信息
git commit -m

gitignore 文件

有些时候我们不想把某些文件纳入版本控制中,比如数据库文件,临时文件,设计文件等

.gitignore 文件 在主目录下建立”.gitignore”文件,此文件有如下规则:

  • git 配置哪些文件忽略 不提交到暂存区中
  • 忽略文件中的空行或以井号(#)开始的行将会被忽略
  • 可以使用Linux通配符。例如:星号(*)代表任意多个字符,问号(?)代表一个字符,方括号([abc])代表可选字符范围,大括号({string1,string2,…})代表可选的字符串等
  • 如果名称的最前面有一个感叹号(!),表示例外规则,将不被忽略
  • 如果名称的最前面是一个路径分隔符(/),表示要忽略的文件在此目录下,而子目录中的文件不忽略
  • 如果名称的最后面是一个路径分隔符(/),表示要忽略的是此目录下该名称的子目录,而非文件(默认文件或目录都忽略)
1
2
3
4
5
6
7
# 为注释
*.txt # 忽略所有 .txt结尾的文件,这样的话上传就不会被选中
!lib.txt # 但lib.txt除外
/temp # 仅忽略项目根目录下的TODO文件,不包括其它目录temp
build/ # 忽略build/目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt

远程仓库使用

github 是有墙的,比较慢,在国内的话,我们一般使用 gitee ,公司中有时候会搭建自己的gitlab服务器

  1. 注册登录平台 gitee github,完善个人信息
  2. 设置本机密钥 SSH 公钥 实现免密码登录 –免登录
    1. 在目录 C:\Users\黎明\.ssh 下打开 Git Bash
    2. 使用命令 ssh-keygen 生成密钥 或 ssh-keygen -t rsa rsa 为加密算法
  3. 将 id_rsa.pub 中公钥信息public key 添加到平台账户中即可
  4. 使用 平台 创建仓库 –研究开源协议

IDEA 集成 Git

  • pull
  • commit
  • push

push到远程仓库

1
2
git push origin master
git push

Git 分支

分支在 Git 中相对较难,分支就是科幻电影里面的平行宇宙,如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,我们就需要处理一些问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看分支
git branch
# 新建 dev 分支
git branch dev

# 列出所有本地分支
git branch
# 列出所有远程分支
git branch -r
# 新建一个分支,但依然停留在当前分支
git branch [branch-name]
# 新建一个分支,并切换到该分支
git checkout -b [branch]
# 合并指定分支到当前分支
git merge [branch]
# 删除分支
git branch -d [branch-name]
# 删除远程分支
git push origin --delete [branch-name]$ git branch -dr [remote/branch]

IDEA中操作

image-20221117012929124 image-20221117012952558

冲突问题

如果同一个文件在合并分支时都被修改了则会引起冲突:解决的办法是我们可以修改冲突文件后重新提交!选择要保留他的代码还是你的代码

master主分支应该非常稳定,用来发布新版本,一般情况下不允许在上面工作,工作一般情况下在新建的dev分支上工作,工作完后,比如上要发布,或者说dev分支代码稳定后可以合并到主分支master上来

6. 细节强化

三种级别的配置

  • local 配置在当前项目的 .git 目录下
  • global 配置在 用户目录的 .gitconfig 文件中
  • system 配置在 git安装目录 /etc/gitconfig 文件中
1
2
3
4
5
6
git config --global user.name zhangsan
git config --global user.email 1031282691@qq.com
git config --unset user.name #默认删除 local 级别下的
git config --global --unset user.name
git config --list
git help

git文件的四种状态

  • unstaged 历史仓库中没有文件记录
  • modified 历史仓库中存在文件记录 但未被修改过
  • staged 文件被保存到暂存区 但未提交到仓库
  • commited 文件被提交到历史仓库

.git 目录中

  • head git项目当前处于的分支
  • config 项目配置信息 –git config --local
  • description 项目描述信息
  • index 索引文件
  • hooks/ 系统默认钩子脚本目录
  • logs/ 各refs的历史信息
  • objects/ git仓库的所有对象 –commit提交/快照 tree目录/指针 tag固定的提交 blob二进制文件
    git 本质是使用了 40 个 16进制的SHA-1 Hash 来 唯一标识对象 tag –> commit –> tree –> tree *** –> blob
  • refs/ 标识目录中每个分支指向哪个commit

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
add 添加文件内容至索引 --暂存区
bisect 通过二分查找定位引入 bug 的变更
branch 列出、创建或删除分支
git branch test 创建测试分支
git checkout test 切换到test分支
合并分支步骤
git checkout master 合并分支到主分支
git merge testtest分支合并到主分支
合并失败-查资料

checkout 检出一个分支或路径到工作区 恢复到分支
git checkout 查资料
clone 克隆一个版本库到一个新目录
commit 提交变更到版本库
diff 显示提交之前、提交和工作区之间的差异 查资料
fetch 从另一个版本库下载对象和引用
grep 输出和模式匹配的行 -过滤
init 创建一个空的git项目 git初始化
git 版本库或重新初始化一个已存在的版本库
log 显示提交日志
merge 合并两个或多个开发历史
mv 移动或重命名
pull
push
rebase 本地提交转移至更新后的上游分支中
reset 重置当前head到指定状态
rm 删除
git rm -f hello.java 删除工作区 暂存区文件 最终历史仓库中文件也会被删除
git rm --cached hello.java 仅删除缓存区中文件 不删除工作区中文件
show 显示各种类型的对象 查看最后一次提交
git show
git show head
git show e0783 根据hash查看内容
git show --pretty=raw e0783 根据hash参数查看对应
git ls-tree 5fb56
staus 显示工作区状态
tag 创建 列出 删除 或 校验 一个 GPG 签名的 tag 对象
打标签,标签使用
git tag "v-0.0.1"
git tag -a "v-0.0.2" -m "project released on v-0.0.02" 具体使用查资料
git remote add s 远程库url
创建一个远程库 简称为s
git fetch s master 不自动合并
git pull s master 自动合并
git push s master 提交
git clone url 拷贝

git commit -m 项目提交描述

重点配置SSH密钥

步骤:

  1. 生成密钥

    • 在目录 C:\Users\黎明\.ssh 下打开 Git Bash
    • 使用命令 ssh-keygen 生成密钥 或 ssh-keygen -t rsa rsa 为加密算法
  2. 配置到代码托管平台

  3. 测试

    1
    2
    $ ssh -T git@github.com
    Hi NegativeFunction! You've successfully authenticated, but GitHub does not provide shell access.

7. 拓展知识

GitLab搭建

GitLab是自建的一个非官方自有版本仓库,提供给公司内部使用

官网:https://about.gitlab.com/downloads

清华镜像:https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7

centOS安装

  1. 安装
1
yum install https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-10.0.0-ce.0.el7.x86_64.rpm
  1. 配置
1
2
3
4
5
6
7
vim /etc/gitlab/gitlab.rb
将external_url变量的地址修改为 gitlab 所在centOS 的ip
因为修改了配置文件,所以重新加载配置内容
gitlab-ctl reconfigure
gitlab-ctl restart
查看日志
gitlab-ctl tail

注意 gitlab 本身还需要启动 jdk tomcat war包安装 所以 建议安装 gitlab 的机器内存 >= 4G

GitLab命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gitlab-ctl command(subcommand)
gitlab-ctl start 启动所有服务
gitlab-ctl stop 关闭所有服务
gitlab-ctl restart 重启所有服务
gitlab-ctl status 查看所有服务状态
gitlab-ctl tail 查看日志信息
gitlab-ctl service-list 列举所有启动服务
gitlab-ctl graceful-kill 平稳停止一个服务
gitlab-ctl help

gitlab-ctl reconfigure 修改配置文件后 重新加载配置文件
gitlab-ctl show-config 验证配置文件
gitlab-ctl restart 重启所有服务

gitlab-ctl show-config 查看所有服务配置文件信息
gitlab-ctl uninstall 卸载这个软件
gitlab-ctl cleanse 删除gitlab所有数据

GitLab携带组件

nginx –静态web服务器

gitlab-shell –用于处理Git命令和修改 authorized keys 列表

gitlab-workhorse –轻量级反向代理服务器

logrotate –日志文件管理工具

postgresql –数据库

redis –缓存数据库

sidekiq –用于后台执行队列任务(异步执行操作)

unicorn –HTTP服务 Gitlab Rails 应用是托管在这个服务器上的

GitLab目录作用

配置文件目录 /etc/gitlab

主配置文件: /etc/gitlab/gitlab.rb –修改配置文件

文档根目录/应用代码和相应的依赖程序: /var/opt/gitlab –存储gitlab-ctl reconfigure 命令编译后的应用数据与配置文件,不需要人为修改配置

默认存储库位置: /var/opt/gitlab/git-data/repositories

Nginx配置文件: /var/opt/gitlab/nginx/conf/gitlab-http.conf

Postgresql数据目录: /var/opt/gitlab/postgresql/data

gitlab各组件日志 /var/log/gitlab

备份生成的目录 /var/opt/gitlab/backups/

8. 实操案例

1
2
3
4
5
git init
git status
git add .
git commit -m 项目初始化
git commit -m 增加用户登录功能

Java8

Java8新特性

Java8特性增强

  • 速度更快 –对于底层的数据结构进行了一定的优化
  • 代码更少 新增加了 lambda表达式语法
  • 新出现了 强大的 Stream API
  • 更便于 并行 –使串行 转换成 并行 更加容易
  • 最大化减少了空指针异常 Optional

==其中最为核心的为 lambda 表达式 与 Stream API==

速度更快体现

HashMap 数组 链表 红黑树 的改变

CAS算法

JVM虚拟机拓展

只有在

Oracle-sun 公司提供的 Hotspot 上才有会在堆内存中 产生一个永久区( 永久区 其中的内容任然会被 垃圾回收机制 回收 只不过 回收的条件比较苛刻) 来作为方法区 而在其他 定制的 JVM(例如 Oracle JRocket、IBM J9 JVM、Taobao JVM) 中早已将 永久区剔除 取而代之的是 从堆中剥离出来形成的 MetaSpace 元空间

MetaSpace 元空间

MetaSpace 元空间 直接使用物理内存 即直接使用 电脑的系统内存 而不经过 JVM 分配内存

从而导致 JVM 参数调优 发生改变

由 PremGenSize 与 MaxPremGenSize 改变为 元空间物理内存 MetaSpaceSize最大元空间物理内存 MaxMetaSpaceSize

1. lambda 表达式

Lambda

lambda 是一个 匿名函数 可以将 lambda 表达式理解为一段可以传递的代码(将代码像数据一样进行传递) 而使用lambda 表达式的目的是为了写出 更简洁 更灵活 的代码 这也使得 lambda 表达式 成为一种更紧凑的代码风格 同时使得Java语言的表达能力得到了极大的提升

lambda表达式简单应用

lambda表达式 解决匿名内部类繁琐编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package lambda;

import org.junit.Test;
import java.util.Comparator;
import java.util.TreeSet;

public class Demo01 {

// 匿名内部类
@Test
public void Test01() {
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};

TreeSet<Integer> treeSet = new TreeSet<>(com);
}

@Test
public void Test02() {
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
Comparator<Integer> com2 = Integer::compare;
TreeSet<Integer> treeSet = new TreeSet<>(com);
}
}

lambda表达式应用于代码进阶优化过程

代码优化进阶 –针对lambda表达式的应用

数据代码

1
2
3
4
5
6
7
8
9
10
11
List<Employee> employees = Arrays.asList(
// 张三李四王五赵六孙七周八吴九郑十
new Employee("张三", 19, 2222.33),
new Employee("李四", 29, 22552.33),
new Employee("王五", 43, 3332.33),
new Employee("赵六", 59, 6255.33),
new Employee("孙七", 64, 7722.33),
new Employee("周八", 25, 8822.33),
new Employee("吴九", 35, 2992.33),
new Employee("郑十", 57, 4992.33)
);

阶段一:

不断写重复方法代码 –针对不同需求 实现 写不同代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 需求 获取当前公司 员工年龄大于 35 的员工信息
*/
public List<Employee> filterEmployees(List<Employee> list) {
List<Employee> emps = new ArrayList<>();

for (Employee emp : list) {
if (emp.getAge() >= 35) {
emps.add(emp);
}
}
return emps;
}

/**
* 需求 获取当前公司 员工工资大于 5000 的员工信息
*/
public List<Employee> filterEmployees2(List<Employee> list) {
List<Employee> emps = new ArrayList<>();

for (Employee emp : list) {
if (emp.getSalary() >= 5000) {
emps.add(emp);
}
}
return emps;
}

阶段二:

策略设计模式优化 –优化方式一 使用接口 配合 策略设计模式

通过 实现接口 MyPredicate 的实例 FilterEmployeeByAge 与 FilterEmployeeBySalary 来控制 需求参数

MyPredicate 接口

1
2
3
4
5
package lambda;

public interface MyPredicate<T> {
public boolean test(T t);
}

FilterEmployeeBySalary 实现类

1
2
3
4
5
6
7
8
9
10
package lambda;

import lambda.pojo.Employee;

public class FilterEmployeeBySalary implements MyPredicate<Employee> {
@Override
public boolean test(Employee t) {
return t.getSalary() >= 5000;
}
}

FilterEmployeeByAge 实现类

1
2
3
4
5
6
7
8
9
10
package lambda;

import lambda.pojo.Employee;

public class FilterEmployeeByAge implements MyPredicate<Employee> {
@Override
public boolean test(Employee t) {
return t.getAge() >= 35;
}
}

Demo02 测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 优化方式一
public List<Employee> filterEmployees(List<Employee> list, MyPredicate<Employee> myPredicate) {
List<Employee> emps = new ArrayList<>();

for (Employee employee : list) {
if (myPredicate.test(employee)) {
emps.add(employee);
}
}

return emps;
}

@Test
public void test02() {
List<Employee> list = filterEmployees(this.employees, new FilterEmployeeByAge());
for (Employee employee : list) {
System.out.println(employee);
}

System.out.println("------------------------------------------------------");

List<Employee> list2 = filterEmployees(this.employees, new FilterEmployeeBySalary());
for (Employee employee : list2) {
System.out.println(employee);
}
}

策略设计模式优化优劣:

业务代码方法只需要一个 通过不同实体类 实现接口方法来进行约束 业务逻辑 对比第一阶段 使业务代码更加 松耦合 更加容易更改 缺点在于 会多出很多的实体类

阶段三:

匿名内部类优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 优化方式三 --匿名内部类方式
@Test
public void test03() {
List<Employee> list = filterEmployees(this.employees, new MyPredicate<Employee>() {
@Override
public boolean test(Employee employee) {
return employee.getAge() < 35;
}
});

for (Employee employee : list) {
System.out.println(employee);
}

}

匿名内部类优化优劣:

动态new 一个匿名内部类 对比于策略设计模式 减少了 需要编写一个实现接口的实体类 从而减少了代码量 但是代码冗余仍然较大

阶段四:

lambda表达式优化

1
2
3
4
5
6
7
8
9
10
// 优化方式四 --lambda表达式优化
@Test
public void test04() {
List<Employee> list = filterEmployees(this.employees, (employee) -> employee.getSalary() < 5000);

for (Employee employee : list) {
System.out.println(employee);
}

}

阶段五:

Stream API 优化 –高级 –Java8新特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 优化方式四 --Stream API 优化
@Test
public void test05() {
employees.stream()
.filter(e -> e.getSalary() >= 5000)
.limit(3)
.forEach(System.out::println);

System.out.println("------------------------------------------------------");

employees.stream()
.map(Employee::getName)
.forEach(System.out::println);

}

Lambda表达式基础语法

Lambda表达式基础语法

Java8中引入了一个新的操作符 -> 该操作符 称为 箭头操作符 或 lambda 操作符

箭头操作符 将 lambda 表达式 拆分成 两个部分

  • 左侧 lambda 表达式的参数列表
  • 右侧 lambda 表达式中所需执行的功能 即 lambda 体 –若接口存在多个 抽象方法 则需要函数式接口的支持

基础语法:

  1. 语法一 – 无参数 无返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @Test
    public void test01() {
    /*
    * 匿名内部类 --局部内部类 如果在匿名内部类(局部内部类)中应用了一个 同级别的 局部变量 在 jdk1.7 之前 必须是使用 final 修饰的常量
    * 但是在 jdk1.8 后会默认给 局部内部类 中使用到的局部变量加上 final 关键字修饰 同样不能改变 只是可以在定义的时候 不用写 final 关键字
    * 故而 在 lambda表达式 中 如果使用了 局部变量 同样会被默认加上 final 关键字修饰 使得其不能被改变
    * */

    int num = 1;

    // 多线程情况下 对比内部类实现
    Runnable r = new Runnable() {
    @Override
    public void run() {
    System.out.println("hello world" + num);
    }
    };

    r.run();

    System.out.println("------------------------------");

    Runnable r2 = () -> System.out.println("hello lambda" + num);

    r2.run();
    }

    注意:匿名内部类 –局部内部类 如果在匿名内部类(局部内部类)中应用了一个 同级别的 局部变量 在 jdk1.7 之前 必须是使用 final 修饰的常量 但是在 jdk1.8 后会默认给 局部内部类 中使用到的局部变量加上 final 关键字修饰 同样不能改变 只是可以在定义的时候 不用写 final 关键字 故而 在 lambda表达式 中 如果使用了 局部变量 同样会被默认加上 final 关键字修饰 使得其不能被改变

  2. 语法二 – 有一个参数 无返回值 若只有一个参数 可省略 左侧参数列表的括号

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void test02() {
    Consumer<String> con = (x) -> System.out.println(x);
    Consumer<String> con3 = x -> System.out.println(x);
    con.accept("hello lambda");
    con3.accept("hello lambda");
    }
  3. 语法三 – 有两个以上参数 有返回值 且 lambda 体中有多条语句

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void test03() {
    Comparator<Integer> com = (x, y) -> {
    System.out.println("hello lambda");
    return Integer.compare(x, y);
    };
    }
  4. 语法四 – 有两个以上参数 有返回值 且 lambda 体中只有一条 return 语句 即可省略 {}return

    1
    2
    3
    4
    5
    @Test
    public void test04() {
    Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
    Comparator<Integer> com2 = Integer::compare;
    }
  5. 语法五 –关于 lambda 表达式的参数列表 编写参数数据类型 可省略不写 JVM编译器 会通过上下文来推断出数据类型 即 “类型推断”

    1
    2
    3
    4
    5
    6
    @Test
    public void test04() {
    Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
    Comparator<Integer> com2 = (Integer x, Integer y) -> Integer.compare(x, y);
    Comparator<Integer> com3 = Integer::compare;
    }

    常见”类型推断”还有

    1
    2
    3
    4
    List<Integer> list = new ArrayList<>();
    List<Integer> list = new ArrayList<>(Integer);

    String[] str = {"222", "22d2", "54363"};

    在java8中 通过上下文来推断出数据类型 得到了很大的升级 举例说明即

    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void test05() {
    show(new HashMap<>()); // 只需要传入 new HashMap<>() 即可 Java8 会根据上下文 推断出需要传入参数的类型
    }

    public void show(Map<String, Integer> map) {

    }

Java8本质上也会有参数类型检查 由于其语法糖性质 故而会使其看起来更加简洁 代码量更加少 但是可读性相对较差

==注意:lambda表达式 需要”函数式接口”的支持 函数式接口 即 只含有一个抽象方法的接口 称之为 函数式接口==

函数式接口可以使用 @FunctionalInterface 注解修饰 标注此注解 能检查 此接口 是否满足成为 函数式接口

参考代码:

Employee

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package lambda.pojo;

public class Employee {

private String name;
private int age;
private double salary;

public Employee() {
}

public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}

@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
'}';
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public double getSalary() {
return salary;
}

public void setSalary(double salary) {
this.salary = salary;
}
}

Demo01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package lambda;

import org.junit.Test;
import java.util.Comparator;
import java.util.TreeSet;

public class Demo01 {

// 匿名内部类
@Test
public void Test01() {
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};

TreeSet<Integer> treeSet = new TreeSet<>(com);
}

@Test
public void Test02() {
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
Comparator<Integer> com2 = Integer::compare;
TreeSet<Integer> treeSet = new TreeSet<>(com);
}
}

Demo02

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package lambda;

import lambda.pojo.Employee;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


public class Demo02 {

List<Employee> employees = Arrays.asList(
// 张三李四王五赵六孙七周八吴九郑十
new Employee("张三", 19, 2222.33),
new Employee("李四", 29, 22552.33),
new Employee("王五", 43, 3332.33),
new Employee("赵六", 59, 6255.33),
new Employee("孙七", 64, 7722.33),
new Employee("周八", 25, 8822.33),
new Employee("吴九", 35, 2992.33),
new Employee("郑十", 57, 4992.33)
);

@Test
public void test01() {
List<Employee> list = filterEmployees(this.employees);
for (Employee employee : list) {
System.out.println(employee);
}

System.out.println("------------------------------------------------------");

List<Employee> list2 = filterEmployees2(this.employees);
for (Employee employee : list2) {
System.out.println(employee);
}

}

@Test
public void test02() {
List<Employee> list = filterEmployees(this.employees, new FilterEmployeeByAge());
for (Employee employee : list) {
System.out.println(employee);
}

System.out.println("------------------------------------------------------");

List<Employee> list2 = filterEmployees(this.employees, new FilterEmployeeBySalary());
for (Employee employee : list2) {
System.out.println(employee);
}
}

// 优化方式一 --策略设计模式
public List<Employee> filterEmployees(List<Employee> list, MyPredicate<Employee> myPredicate) {
List<Employee> emps = new ArrayList<>();

for (Employee employee : list) {
if (myPredicate.test(employee)) {
emps.add(employee);
}
}

return emps;
}

// 优化方式二 --匿名内部类方式
@Test
public void test03() {
List<Employee> list = filterEmployees(this.employees, new MyPredicate<Employee>() {
@Override
public boolean test(Employee employee) {
return employee.getAge() < 35;
}
});

for (Employee employee : list) {
System.out.println(employee);
}

}

// 优化方式三 --lambda表达式优化
@Test
public void test04() {
List<Employee> list = filterEmployees(this.employees, (employee) -> employee.getSalary() < 5000);

for (Employee employee : list) {
System.out.println(employee);
}

}

// 优化方式四 --Stream API 优化
@Test
public void test05() {
employees.stream()
.filter(e -> e.getSalary() >= 5000)
.limit(3)
.forEach(System.out::println);

System.out.println("------------------------------------------------------");

employees.stream()
.map(Employee::getName)
.forEach(System.out::println);

}

/**
* 需求 获取当前公司 员工年龄大于 35 的员工信息
*/
public List<Employee> filterEmployees(List<Employee> list) {
List<Employee> emps = new ArrayList<>();

for (Employee emp : list) {
if (emp.getAge() >= 35) {
emps.add(emp);
}
}
return emps;
}

/**
* 需求 获取当前公司 员工工资大于 5000 的员工信息
*/
public List<Employee> filterEmployees2(List<Employee> list) {
List<Employee> emps = new ArrayList<>();

for (Employee emp : list) {
if (emp.getSalary() >= 5000) {
emps.add(emp);
}
}
return emps;
}
}

Demo03

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package lambda;

import lambda.pojo.Employee;
import org.junit.Test;

import java.util.*;
import java.util.function.Consumer;

/**
* Java8中引入了一个新的操作符 `->` 该操作符 称为 箭头操作符 或 lambda 操作符
*
* 箭头操作符 将 lambda 表达式 拆分成 两个部分
*
* + 左侧 lambda 表达式的参数列表
* + 右侧 lambda 表达式中所需执行的功能 即 lambda 体 --若接口存在多个 抽象方法 则需要函数式接口的支持
*
* 语法一: 无参数 无返回值
* () -> System.out.println("Hello lambda");
*
* 语法二: 有一个参数 无返回值 --若只有一个参数 可省略 左侧参数列表的括号
* con = (x) -> System.out.println(x);
*
* 语法三: 有两个以上参数 有返回值 且 lambda 体中有多条语句
* Comparator<Integer> com = (x, y) -> {
* System.out.println("hello lambda");
* return Integer.compare(x, y);
* };
*
* 语法四: 有两个以上参数 有返回值 且 lambda 体中只有一条 return 语句 即可省略 {} 与 return
* Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
*
* 语法五: 关于 lambda 表达式的参数列表 编写参数数据类型 可省略不写 JVM编译器 会通过上下文来推断出数据类型 即 "类型推断"
* Comparator<Integer> com2 = (Integer x, Integer y) -> Integer.compare(x, y);
*
*/
public class Demo03 {

List<Integer> list = new ArrayList<>();
String[] str = {"222", "22d2", "54363"};

@Test
public void test01() {
/*
* 匿名内部类 --局部内部类 如果在匿名内部类(局部内部类)中应用了一个 同级别的 局部变量 在 jdk1.7 之前 必须是使用 final 修饰的常量
* 但是在 jdk1.8 后会默认给 局部内部类 中使用到的局部变量加上 final 关键字修饰 同样不能改变 只是可以在定义的时候 不用写 final 关键字
* 故而 在 lambda表达式 中 如果使用了 局部变量 同样会被默认加上 final 关键字修饰 使得其不能被改变
* */

int num = 1;

// 多线程情况下 对比内部类实现
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("hello world" + num);
}
};
r.run();

System.out.println("------------------------------");

Runnable r2 = () -> System.out.println("hello lambda" + num);
r2.run();
}

@Test
public void test02() {
Consumer<String> con = (x) -> System.out.println(x);
Consumer<String> con3 = x -> System.out.println(x);
con.accept("hello lambda");
}

@Test
public void test03() {
Comparator<Integer> com = (x, y) -> {
System.out.println("hello lambda");
return Integer.compare(x, y);
};
}

@Test
public void test04() {
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
Comparator<Integer> com2 = (Integer x, Integer y) -> Integer.compare(x, y);
Comparator<Integer> com3 = Integer::compare;
}

@Test
public void test05() {
show(new HashMap<>());
}

public void show(Map<String, Integer> map) {

}

}

FilterEmployeeByAge

1
2
3
4
5
6
7
8
9
10
package lambda;

import lambda.pojo.Employee;

public class FilterEmployeeByAge implements MyPredicate<Employee> {
@Override
public boolean test(Employee t) {
return t.getAge() >= 35;
}
}

FilterEmployeeBySalary

1
2
3
4
5
6
7
8
9
10
package lambda;

import lambda.pojo.Employee;

public class FilterEmployeeBySalary implements MyPredicate<Employee> {
@Override
public boolean test(Employee t) {
return t.getSalary() >= 5000;
}
}

MyPredicate

1
2
3
4
5
package lambda;

public interface MyPredicate<T> {
public boolean test(T t);
}

lambda表达式练习

练习一

数字计算操作

1
2
3
4
5
6
7
8
9
10
@Test
public void test01() {
System.out.println(operation(122, x -> x * 2));
System.out.println(operation(122, y -> y - 2));
System.out.println(operation(122, z -> z + 233));
}

public Integer operation(Integer num, MyFunction mf) {
return mf.getValue(num);
}

MyFunction 接口

1
2
3
4
5
6
package lambda;

@FunctionalInterface
public interface MyFunction {
public Integer getValue(Integer num);
}

练习二

调用 Collections.sort() 方法 通过定制排序比较两个 Employee(先按年龄比 年龄相同 按姓名比) 使用lambda 作为参数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test02() {
Collections.sort(employees, (e1, e2) -> {
if (e1.getAge() == e2.getAge()) {
return e1.getName().compareTo(e2.getName());
} else {
return -Integer.compare(e1.getAge(), e2.getAge());
}
});

for (Employee employee : employees) {
System.out.println(employee);
}
}

练习三

  • 声明函数式接口 在接口中声明抽象方法 public String getValue(String str);
  • 声明类 Demo04 在类中编写方法使用接口作为参数 将要给字符串转换成大写 并作为方法的返回值
  • 再将一个字符串的 第 2 个 和 第 4 个 索引位置进行截取字串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test03() {
String s = strHandler("\t\t\t hello world", String::trim);
String s2 = strHandler("\t\t\t hello world", str -> str.trim());
System.out.println(s);

String s3 = strHandler("asdgsd", String::toUpperCase);
String s4 = strHandler("asdgsd", str -> str.toUpperCase());
System.out.println(s4);

System.out.println("--------------");

String s5 = strHandler("Lineyuxanhan", str -> str.substring(1, 4));
System.out.println(s5);

}

public String strHandler(String str, MyFun mf) {
return mf.getValue(str);
}

MyFun 接口

1
2
3
4
5
6
package lambda;

@FunctionalInterface
public interface MyFun {
public String getValue(String str);
}

练习四

  • 声明一个带两个泛型的函数式接口 泛型类型为 <T, R> T 为参数 R 为返回值
  • 接口中声明对应抽象方法
  • 在 TestLambda 类中 声明方法 使接口作为参数 计算两个 long 类型 参数的和
  • 再计算两个 long 类型 参数的乘积
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test04() {
operation(100L, 200L, (x, y) -> x + y);
System.out.println("-------------------");
operation(100L, 200L, (x, y) -> x - y);
System.out.println("-------------------");
operation(100L, 200L, Long::sum);
System.out.println("-------------------");
operation(100L, 200L, (x, y) -> x * y);
}

public void operation(Long l1, Long l2, MyFun2<Long, Long> mf) {
System.out.println(mf.getValue(l1, l2));
}

MyFun2

1
2
3
4
5
6
package lambda;

@FunctionalInterface
public interface MyFun2<T, R> {
public R getValue(T t1, T t2);
}

参考代码

Demo04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package lambda;

import lambda.pojo.Employee;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Demo04 {

List<Employee> employees = Arrays.asList(
// 张三李四王五赵六孙七周八吴九郑十
new Employee("张三", 19, 2222.33),
new Employee("王五", 29, 22552.33),
new Employee("李四", 29, 3332.33),
new Employee("赵六", 59, 6255.33),
new Employee("孙七", 64, 7722.33),
new Employee("周八", 25, 8822.33),
new Employee("吴九", 35, 2992.33),
new Employee("郑十", 57, 4992.33)
);

@Test
public void test01() {
System.out.println(operation(122, x -> x * 2));
System.out.println(operation(122, y -> y - 2));
System.out.println(operation(122, z -> z + 233));
}

public Integer operation(Integer num, MyFunction mf) {
return mf.getValue(num);
}

@Test
public void test02() {
Collections.sort(employees, (e1, e2) -> {
if (e1.getAge() == e2.getAge()) {
return e1.getName().compareTo(e2.getName());
} else {
return -Integer.compare(e1.getAge(), e2.getAge());
}
});

for (Employee employee : employees) {
System.out.println(employee);
}
}

@Test
public void test03() {
String s = strHandler("\t\t\t hello world", String::trim);
String s2 = strHandler("\t\t\t hello world", str -> str.trim());
System.out.println(s);

System.out.println("--------------");

String s3 = strHandler("asdgsd", String::toUpperCase);
String s4 = strHandler("asdgsd", str -> str.toUpperCase());
System.out.println(s4);

System.out.println("--------------");

String s5 = strHandler("Lineyuxanhan", str -> str.substring(1, 4));
System.out.println(s5);

}

// 处理字符串
public String strHandler(String str, MyFun mf) {
return mf.getValue(str);
}

@Test
public void test04() {
operation(100L, 200L, (x, y) -> x + y);
System.out.println("-------------------");
operation(100L, 200L, (x, y) -> x - y);
System.out.println("-------------------");
operation(100L, 200L, Long::sum);
System.out.println("-------------------");
operation(100L, 200L, (x, y) -> x * y);
}

public void operation(Long l1, Long l2, MyFun2<Long, Long> mf) {
System.out.println(mf.getValue(l1, l2));
}

}

2. 函数式接口

Java8 常见四大内置核心函数式接口 –用于lambda表达式

消费型接口 Consumer<T>

1
2
Consumer<T>
void accept(T t);

供给型接口 Supplier<T>

1
2
Supplier<T>
T get();

函数型接口 Function<T, R>

1
2
Function<T, R>
R apply(T t);

断言型接口 Predicate<T>

1
2
Predicate<T>
boolean test(T t);

参考代码

Demo05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package lambda;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.*;

/**
* Java8 中 四大内置核心函数式接口
*
* Consumer<T> 消费型接口
* void accept(T t);
*
* Supplier<T> 供给型接口
* T get();
*
* Function<T, R> 函数型接口
* R apply(T t);
*
* Predicate<T> 断言型接口
* boolean test(T t);
*/
public class Demo05 {


// Consumer<T> 消费型接口
@Test
public void test01() {
happy(10000, m -> System.out.println("本次消费金额" + m + "元"));
}

// 需求 消费多少元
public void happy(double money, DoubleConsumer con) {
con.accept(money);
}

public void happy2(double money, Consumer<Double> con) {
con.accept(money);
}

// Supplier<T> 供给型接口
@Test
public void test02() {
List<Integer> numList = getNumList(10, () -> (int)(Math.random() * 100));
for (Integer num : numList) {
System.out.println(num);
}

}

// 需求 产生指定 n 个整数 并存放到集合中
public List<Integer> getNumList(int num, Supplier<Integer> sup) {
List<Integer> list = new ArrayList<>();

for (int i = 0; i < num; i++) {
Integer n = sup.get();
list.add(n);
}

return list;
}

// Function<T, R> 函数型接口
@Test
public void test03() {
System.out.println(strHandler("Linyuxuan", String::toUpperCase));
System.out.println("-------------");
System.out.println(strHandler("Kking_Queen", s -> s.length() + ""));
System.out.println("-------------");
System.out.println(strHandler("Kking_Queen", s -> {
String s1 = s.toLowerCase();
s1 += "nihaoya";
return s1.toUpperCase();
}));
}

// 需求 用于处理字符串
public String strHandler(String str, Function<String, String> fun) {
return fun.apply(str);
}

// Predicate<T> 断言型接口
@Test
public void test04() {
List<String> list = Arrays.asList("Hello", "nihao", "Kking", "KLus", "Winas", "K&K is win", "Queen", "LIny");
System.out.println(filterStr(list, s -> s.length() > 4));
}

// 需求 过滤满足条件的字符串 并存入集合中
public List<String> filterStr(List<String> list, Predicate<String> pre) {
List<String> strList = new ArrayList<>();

for (String str : list) {
if (pre.test(str)) {
strList.add(str);
}
}
return strList;
}
}

TODO

image-20221007212140246

3. 方法引用 构造器引用 与 数组引用

方法引用

方法引用 即当 lambda 体中的内容 方法已经被实现了 就可以使用 “方法引用” 来直接 编写使用

简单理解即 方法引用是 lambda 表达式的另外一种表现形式

语法格式

  • 语法格式一 对象::实例方法名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 对象::实例方法名
    @Test
    public void test01() {
    // 使用条件 必须 接口 参数列表 返回值 与 实例方法 参数列表 返回值 类型 一致
    Consumer<String> con = x -> System.out.println(x);
    Consumer<String> con2 = System.out::println;

    PrintStream ps1 = System.out;
    Consumer<String> con3 = x -> ps1.println(x);

    PrintStream ps2 = System.out;
    Consumer<String> con4 = ps2::println;

    con.accept("hello world");
    con2.accept("hello world2");
    con3.accept("hello world3");
    con4.accept("hello world4");
    }

    @Test
    public void test02() {
    Employee emp = new Employee("zhangsan", 18, 255);
    Supplier<String> sup = () -> emp.getName();
    String str = sup.get();
    System.out.println(str);

    Supplier<String> sup2 = emp::getName;
    Supplier<Integer> sup3 = emp::getAge;

    String str2 = sup2.get();
    System.out.println(str2);

    Integer num = sup3.get();
    System.out.println(num);
    }
  • 语法格式二 类::静态方法名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 类::静态方法名
    @Test
    public void test03() {
    // 使用条件 必须 接口 参数列表 返回值 与 类的静态方法 参数列表 返回值 类型 一致
    Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
    Comparator<Integer> com2 = Integer::compare;

    System.out.println(com.compare(4, 2));
    System.out.println(com2.compare(12, 23));
    }
  • 语法格式三 类::实例方法名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 类::实例方法名
    @Test
    public void test04() {
    // 使用条件较为苛刻
    BiPredicate<String, String> bp = (x, y) -> x.equals(y);
    System.out.println(bp.test("dddd", "ffff"));

    // 只有当 类 中的实例方法 包含两个参数 x y 并且 lambda 体中 调用方法为 x.function(y) 才能满足使用要求
    BiPredicate<String, String> bp2 = String::equals;
    System.out.println(bp2.test("dddd", "dddd"));
    }

构造器引用

构造器引用

语法 ClassName::new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test05() {
Supplier<Employee> sup = () -> new Employee();

// 注意 使用条件为 构造器的参数列表 必须与 函数式接口中抽象方法的参数列表保持一致
// 构造器引用 --调用无参构造
Supplier<Employee> sup2 = Employee::new;

// 构造器引用 --调用有参构造
Function<String, Employee> fun = (x) -> new Employee();
Function<String, Employee> fun2 = Employee::new;

System.out.println(fun.apply("zhangsan"));
System.out.println(fun2.apply("lisi"));
}

数组引用

语法:Type[]::new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 数组引用
@Test
public void test06() {
Function<Integer, String[]> fun = x -> new String[x];
String[] strs = fun.apply(10);
for (String str : strs) {
System.out.println(str);
}
System.out.println(strs.length);

Function<Integer, String[]> fun2 = String[]::new;
String[] strs2 = fun2.apply(20);
System.out.println(strs2.length);
}

参考代码

Demo06

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package lambda;

import lambda.pojo.Employee;
import org.junit.Test;

import java.io.PrintStream;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class Demo06 {

// 对象::实例方法名
@Test
public void test01() {
// 使用条件 必须 接口 参数列表 返回值 与 对象实例方法 参数列表 返回值 类型 一致
Consumer<String> con = x -> System.out.println(x);
Consumer<String> con2 = System.out::println;

PrintStream ps1 = System.out;
Consumer<String> con3 = x -> ps1.println(x);

PrintStream ps2 = System.out;
Consumer<String> con4 = ps2::println;

con.accept("hello world");
con2.accept("hello world2");
con3.accept("hello world3");
con4.accept("hello world4");
}

@Test
public void test02() {
Employee emp = new Employee("zhangsan", 18, 255);
Supplier<String> sup = () -> emp.getName();
String str = sup.get();
System.out.println(str);

Supplier<String> sup2 = emp::getName;
Supplier<Integer> sup3 = emp::getAge;

String str2 = sup2.get();
System.out.println(str2);

Integer num = sup3.get();
System.out.println(num);
}

// 类::静态方法名
@Test
public void test03() {
// 使用条件 必须 接口 参数列表 返回值 与 类的静态方法 参数列表 返回值 类型 一致
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
Comparator<Integer> com2 = Integer::compare;

System.out.println(com.compare(4, 2));
System.out.println(com2.compare(12, 23));
}

// 类::实例方法名
@Test
public void test04() {
// 使用条件较为苛刻
BiPredicate<String, String> bp = (x, y) -> x.equals(y);
System.out.println(bp.test("dddd", "ffff"));

// 只有当 类 中的实例方法 包含两个参数 x y 并且 lambda 体中 调用方法为 x.function(y) 才能满足使用要求
BiPredicate<String, String> bp2 = String::equals;
System.out.println(bp2.test("dddd", "dddd"));
}

// 构造器引用
@Test
public void test05() {

Supplier<Employee> sup = () -> new Employee();

// 注意 使用条件为 构造器的参数列表 必须与 函数式接口中抽象方法的参数列表保持一致
// 构造器引用 --调用无参构造
Supplier<Employee> sup2 = Employee::new;

// 构造器引用 --调用有参构造
Function<String, Employee> fun = (x) -> new Employee();
Function<String, Employee> fun2 = Employee::new;

System.out.println(fun.apply("zhangsan"));
System.out.println(fun2.apply("lisi"));
}

// 数组引用
@Test
public void test06() {
Function<Integer, String[]> fun = x -> new String[x];
String[] strs = fun.apply(10);
for (String str : strs) {
System.out.println(str);
}
System.out.println(strs.length);

Function<Integer, String[]> fun2 = String[]::new;
String[] strs2 = fun2.apply(20);
System.out.println(strs2.length);
}
}

4. Stream API

Sream 是 Java8 中处理集合关键抽象概念 可以指定你希望对集合进行的操作 可以执行非常复杂的查找 过滤 和 映射数据等操作 使用Stream API 对集合数据 进行操作 就类似于 使用SQL执行的数据库查询 同时 Stream API 也提供并行执行操作 简单来说 Stream API 提供了一种高效且易于使用数据处理方式

Stream API 工作模式

数据源(集合、数组等….) 通过 进行一系列流水线式的中间操作 形成一个新的数据流 (新 集合、数组….)

==注意:数据源是不会发生任何改变的==

Sream 流

java8中的 Stream 特指的是 数据渠道 (数据流) 用于操作数据源 (集合、数组等) 所生成的元素列表 –集合讲的是 数据 而流 讲的是计算

==注意:==

  • Stream 自身不会存储数据
  • Stream API 存在 内部迭代 由 Stream API 自己完成
  • Stream 不会改变数据源对象 相反 经过Stream API 操作后 会返回一个持有新结果的新Stream
  • Stream 操作是延迟执行的 意味着 他们会等到需要结果 (执行终止操作时)的时候才执行 (中间操作) (有点类似于 懒加载 –个人理解)

Stream具体操作步骤:

  • 创建 Stream
    一个数据源 (集合、数组等….) 获取一个流

    创建Stream的四种方式

    1. 通过 Collection 系列集合提供的 stream()parallelStream() 创建Stream
    2. 通过 Arrays 工具类中的静态方法 stream() 获取数组流
    3. 通过 Stream 类中的静态方法 of() 创建Stream
    4. 创建无限流
  • 中间操作
    一个中间操作链 对数据源的数据进行处理
    多个中间操作可以连接起来 形成一个流水线 除非流水线上出发终止操作 否则中间操作不会执行任何的处理 而是在终止操作时一次性全部处理 –称为 “惰性求值

    • 筛选与切片
    • 映射
    • 排序
  • 终止操作 (终端操作)
    一个终止操作 执行中间操作的操作链 并产生结果

    • 查找与匹配
    • 归约与收集

image-20221008080443921

参考代码

Demo07

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
package lambda;

import lambda.pojo.Employee;
import org.junit.Test;

import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Demo07 {

List<Employee> employees = Arrays.asList(
// 张三李四王五赵六孙七周八吴九郑十
new Employee("张三", 19, 2222.33),
new Employee("李四", 29, 22552.33),
new Employee("王五", 43, 3332.33),
new Employee("赵六", 59, 6255.33),
new Employee("孙七", 64, 7722.33),
new Employee("周八", 25, 8822.33),
new Employee("吴九", 35, 2992.33),
new Employee("赵九", 35, 2992.33),
new Employee("吴九", 35, 2992.33),
new Employee("郑十", 57, 4992.33)
);

List<Employee> employees2 = Arrays.asList(
// 张三李四王五赵六孙七周八吴九郑十
new Employee("张三", 19, 2222.33, Employee.Status.FREE),
new Employee("李四", 29, 2552.33, Employee.Status.VOCATION),
new Employee("王五", 43, 3332.33, Employee.Status.FREE),
new Employee("赵六", 59, 6255.33, Employee.Status.VOCATION),
new Employee("孙七", 64, 7722.33, Employee.Status.VOCATION),
new Employee("周八", 25, 8822.33, Employee.Status.FREE),
new Employee("吴九", 35, 2992.33, Employee.Status.BUSY),
new Employee("赵九", 35, 2992.33, Employee.Status.BUSY),
new Employee("吴九", 35, 2992.33, Employee.Status.BUSY),
new Employee("郑十", 57, 4992.33, Employee.Status.FREE)
);

@Test
public void test01() {
// 通过 Collection 系列集合提供的 stream() 或 parallelStream()
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();

// 通过 Arrays 中的静态方法 stream() 获取 数组流
Employee[] emps = new Employee[10];
Stream<Employee> stream2 = Arrays.stream(emps);

// 通过 Stream 类中的静态方法 of()
Stream<String> stream3 = Stream.of("aa", "bb", "cc");

// 创建 无限流
// 迭代
Stream<Integer> stream4 = Stream.iterate(0, x -> x + 2);
stream4.limit(50)
.forEach(System.out::println);

System.out.println("----------------------------");

// 生成
Stream.generate(() -> Math.random())
.limit(30)
.forEach(System.out::println);

}

// 详解 中间操作 --筛选与切片 --映射 --排序

/**
* 筛选与切片
* filter --接收 lambda 从流中排除某些元素
* limit --截断流 使其元素不超过一定数量
* skip(n) --跳过元素 返回一个 跳过前 n 个元素的流 若流中元素不足 n 个 则返回一个空流 与 limit(n) 互补
* distinct --筛选 通过流所生成元素的 hashCode() 和 equals() 去除重复元素
*/
@Test
public void test02() {
Stream<Employee> stream = employees.stream()
// 中间操作 过滤
//.filter(e -> e.getAge() > 30);
// 使用 limit 仅会找到 迭代到 满足条件的结果后 就会短路不执行下面的跌倒 --一定程度上减低了 时间复杂度
.filter(e -> {
System.out.println("中间操作");
return e.getAge() > 30;
})
.limit(2);
/*
执行结果为:
中间操作
中间操作
中间操作
Employee{name='王五', age=43, salary=3332.33}
中间操作
Employee{name='赵六', age=59, salary=6255.33}
=> 原因: 在迭代到满足结果后 就不会在往下迭代 故而不继续进行操作 --类似于 短路 && 与 短路 || 用于提高效率
*/
// 终止操作 打印结果
stream.forEach(System.out::println);

System.out.println("----------------");

// 使用终止操作优化
employees.stream()
.filter(e -> e.getSalary() > 3000)
.limit(2)
.forEach(System.out::println);

System.out.println("----------------");

// 实现 skip 与 limit 互补
employees.stream()
.filter(e -> e.getSalary() > 3000)
//.limit(3)
.skip(3)
.forEach(System.out::println);

System.out.println("----------------------------");

employees.stream()
// distinct() 通过 hashCode 与 equals 来进行去重 故而需要重写两个方法
.distinct()
.forEach(System.out::println);
}

// 测试 Stream 流的延时性
@Test
public void test03() {
Stream<Employee> stream = employees.stream()
// 中间操作 过滤 --若不进行终止操作 即不会执行任何操作
// 内部迭代 由 Stream API 自己完成 不需要我们执行
.filter(e -> {
System.out.println("你好吖");
return e.getSalary() > 5000;
});
// 终止操作 打印结果
// 不进行终止操作 就不会进行中间操作 故而此 test03 执行没有任何结果 -- "惰性求值"
//stream.forEach(System.out::println);
}

@Test
public void test04() {
// 测试外部迭代 自主实现迭代 --通过 迭代器 进行迭代

Iterator<Employee> it = employees.iterator();

while (it.hasNext()) {
System.out.println(it.next());
}

System.out.println("--------------");

for (Employee employee : employees) {
System.out.println(employee);
}
}

/**
* 映射
* map --接收 lambda 将元素转换成其他形式 或 提取信息 本质即将一个函数作为参数 该函数会被应用到每个元素上 并将其映射成一个新的元素
* flatMap --接收一个函数作为参数 将流中的每个值都转换成另一个流 然后将所有流连接起来
*/
@Test
public void test05() {
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg");

list.stream()
.map(str -> str.toUpperCase())
.forEach(System.out::println);

System.out.println("------------------");

list.stream()
.map(String::toUpperCase)
.forEach(System.out::println);

System.out.println("------------------");

employees.stream()
.map(Employee::getName)
.forEach(System.out::println);

System.out.println("------------------");

Stream<Stream<Character>> stream = list.stream()
.map(Demo07::filterCharacter);

//System.out.println(stream);
stream.forEach(sm -> sm.forEach(System.out::println));

System.out.println("------------------");

// 循环遍历嵌套流 较为复杂繁琐 引入 flatMap 将流合并返回
// 表现形式 类似于 add(Object obj) 与 addAll(Collection col)
list.stream()
.flatMap(Demo07::filterCharacter)
.forEach(System.out::println);

}

// 需求 将字符串中的字符提取出来 并存储到列表 以 Stream 流形式返回
public static Stream<Character> filterCharacter(String str) {
List<Character> list = new ArrayList<>();

for (Character ch : str.toCharArray()) {
list.add(ch);
}
return list.stream();
}

// 佐证 add(Object obj) 与 addAll(Collection col)
@Test
public void test06() {
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg");
List<Object> list2 = new ArrayList<>();

list2.add("nihaoya");
list2.add(list);
list2.addAll(list);
list2.add("xiaokuangshen");

System.out.println(list2);
}

/**
* 排序
* sorted() --自然排序 (Comparable)
* sorted(Comparator com) --定制排序 (Comparator)
*/
@Test
public void test07() {
List<String> list = Arrays.asList("ccc", "fff", "bbb", "ddd", "aaa", "eee", "ggg");
list.stream()
.sorted()
.forEach(System.out::println);

System.out.println("------------------");

employees.stream()
.sorted((e1, e2) -> {
if (e1.getAge() == e2.getAge()) {
return -e1.getName().compareTo(e2.getName());
} else {
return -Integer.compare(e1.getAge(), e2.getAge());
}
}).forEach(System.out::println);

}

// 详解 终止操作 --查找与匹配
/**
* 查找与匹配
* allMatch --检查是否匹配所有元素
* anyMatch --检查是否至少匹配一个元素
* noneMatch --检查是否没有匹配所有元素 -遍历结束一个都没有匹配上
* findFirst --返回第一个元素
* findAny --返回当前流中的任意元素
* count --返回流中元素的总个数
* max --返回流中最大值
* min --返回流中最小值
* forEach --内部迭代 StreamAPI 使用内部迭代 会将 Stream 流中的数据 自己进行迭代运算
* 而 使用 Collection 接口则需要用户自己去做迭代 称为 外部迭代
*/

@Test
public void test08() {
boolean r1 = employees2.stream()
.allMatch(e -> e.getStatus().equals(Employee.Status.BUSY));

boolean r2 = employees2.stream()
.anyMatch(e -> e.getStatus().equals(Employee.Status.BUSY));

boolean r3 = employees2.stream()
.noneMatch(e -> e.getStatus().equals(Employee.Status.BUSY));

System.out.println(r1);
System.out.println(r2);
System.out.println(r3);
System.out.println("-------------------------");

Optional<Employee> o1 = employees2.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary))
.findFirst();

Optional<Employee> o2 = employees2.stream()
.sorted((e1, e2) -> -Double.compare(e1.getSalary(), e2.getSalary()))
.findFirst();

Optional<Employee> o3 = employees2.stream().min(Comparator.comparingDouble(Employee::getSalary));

Optional<Employee> o4 = employees2.stream()
.filter(e -> e.getStatus().equals(Employee.Status.FREE))
.findAny();

Optional<Employee> o5 = employees2.parallelStream()
.filter(e -> e.getStatus().equals(Employee.Status.FREE))
.findAny();

System.out.println(o1.get());
System.out.println(o2);
System.out.println(o3);

System.out.println("-------------------------");

System.out.println(o4.get());

System.out.println("-------------------------");

// 串行流 与 并行流 串行流是通过迭代 依次寻找 而并行流是通过 多线程 同时寻找
System.out.println(o5.get());

System.out.println("-------------------------");

long count = employees2.stream()
.count();
long count2 = employees2.size();

System.out.println(count);
System.out.println(count2);

Optional<Employee> o6 = employees2.stream()
.max((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));

Optional<Employee> o7 = employees2.stream()
.min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));

System.out.println(o6.get());
System.out.println(o7.get());

// 对比 直接 映射 工资 比较工资大小
Optional<Double> o8 = employees2.stream()
.map(Employee::getSalary)
.max(Double::compare);

System.out.println(o8);
}

/**
* 归约
* reduce(T identity, BinaryOperator)
* reduce(BinaryOperator) --可以将流中元素反复结合起来 得到一个值
* map 和 reduce 的连接 通常称为 map-reduce 模式 因 Google 用它来进行网络搜索而出名
*/
@Test
public void test09() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Integer r1 = list.stream()
.reduce(0, (x, y) -> x + y);
System.out.println(r1);

// 由于 给定 identity 值 故而 Stream流返回值 不可能为空 所以使用 Integer 接就行
Integer r2 = list.stream()
.reduce(0, Integer::sum); // 先将 0 作为 x 将 1 作为 y 进行相加后 赋值到 x 再 将 2 作为 y 依次累加
System.out.println(r2);

System.out.println("reduce(T identity, BinaryOperator) 对比学习 reduce(BinaryOperator)");

// 由于 不给定 identity 值 故而 Stream流返回值 可能为空 故而需要使用 Optional 来接返回值 达到避免 OPE 的优化
Optional<Double> o1 = employees2.stream()
// map-reduce 模式 用来检索
.map(Employee::getSalary)
.reduce(Double::sum);

System.out.println(o1.get());
}

/**
* 收集
* collect 将流转换为其他形式 接收一个 Collector 接口的实现 用于给 Stream 中元素做汇总的方法
* Collector 接口中方法的实现决定了如何对 流 执行收集操作 (如收集到 List、Set、Map) 但是 Collectors 实用工具类 提供了很多静态方法
* 来方便我们创建常用的收集器实例 具体方法
*/
@Test
public void test10() {
List<String> collect1 = employees2.stream()
.map(Employee::getName)
.collect(Collectors.toList());
System.out.println(collect1);

System.out.println("---------------------");

Set<String> collect2 = employees2.stream()
.map(Employee::getName)
.collect(Collectors.toSet());
System.out.println(collect2);

System.out.println("---------------------");

Set<String> collect3 = employees2.stream()
.map(Employee::getName)
.collect(Collectors.toCollection(HashSet::new));
System.out.println(collect3);

System.out.println("---------------------");

// 计算总数
Long collect4 = employees2.stream()
.collect(Collectors.counting());
System.out.println(collect4);
System.out.println("---------------------");

// 平均数
Double collect5 = employees2.stream()
.collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(collect5);

// 总和
Double collect6 = employees2.stream()
.collect(Collectors.summingDouble(Employee::getSalary));
Double collect7 = employees2.stream().mapToDouble(Employee::getSalary).sum();
System.out.println(collect6);
System.out.println(collect7);

// 工资最大值
Optional<Employee> collect8 = employees2.stream()
.collect(Collectors.maxBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(collect8.get());

// 工资最小值
Optional<Employee> collect9 = employees2.stream()
.collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(collect9.get());

System.out.println("--------------------------");

// 分组
Map<Employee.Status, List<Employee>> collect10 = employees2.stream()
.collect(Collectors.groupingBy(Employee::getStatus));
System.out.println(collect10);

System.out.println("--------------------------");

// 多级分组
Map<Employee.Status, Map<String, List<Employee>>> collect11 = employees2.stream()
.collect(Collectors.groupingBy(Employee::getStatus, Collectors.groupingBy(e -> {
if (e.getAge() <= 30) {
return "青年";
} else if (e.getAge() <= 50) {
return "中年";
} else {
return "老年";
}
})));

System.out.println(collect11);

System.out.println("----------------");

// 分区
Map<Boolean, List<Employee>> collect12 = employees2.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 5000));
System.out.println(collect12);

// summarizingDouble 应用
DoubleSummaryStatistics collect13 = employees2.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println(collect13.getAverage());
System.out.println(collect13.getCount());
System.out.println(collect13.getSum());
System.out.println(collect13.getMax());
System.out.println(collect13.getMin());

// 连接字符串
String collect14 = employees2.stream()
.map(Employee::getName)
.collect(Collectors.joining("-", "first", "end"));
System.out.println(collect14);
}
}

简单练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package lambda;

import lambda.pojo.Employee;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class Demo08 {

List<Employee> employees = Arrays.asList(
// 张三李四王五赵六孙七周八吴九郑十
new Employee("张三", 19, 2222.33),
new Employee("李四", 29, 22552.33),
new Employee("王五", 43, 3332.33),
new Employee("赵六", 59, 6255.33),
new Employee("孙七", 64, 7722.33),
new Employee("周八", 25, 8822.33),
new Employee("吴九", 35, 2992.33),
new Employee("赵九", 35, 2992.33),
new Employee("吴九", 35, 2992.33),
new Employee("郑十", 57, 4992.33)
);

// Stream API 练习
/*
1. 给定一个数字列表 返回一个由每个数字平方构成的列表
[1, 2, 3, 4, 5, 6] --> [1, 4, 9, 16, 25, 36]
*/
@Test
public void test01() {
Integer[] arr = new Integer[]{1, 2, 3, 4, 5, 6};
List<Integer> collect01 = Arrays.stream(arr)
.map(x -> x * x)
.collect(Collectors.toList());
System.out.println(collect01);
}

/*
2. 使用 map 和 reduce 方法来数一数 流 中有多少个Employee
*/
@Test
public void test02() {
Optional<Integer> collect01 = employees.stream()
.map(e -> 1)
.reduce(Integer::sum);
System.out.println(collect01.get());
}
}

并行流 与 顺序流

并行流 就是将一块数据内容分成多个数据块 并用不同的线程分别处理每个数据块的流

Java8 中将并行流进行优化 使对数据进行并行操作更加便捷 同时StreamAPI 可以声明性的通过 parallel()sequential() 在并行流 与 顺序流之间进行切换

理解 Fork / Join 框架

理解 Fork / Join 框架 本质就是在必要情况下 将一个 大任务 进行拆分 (fork) 成若干个 小任务 (拆到不可再拆时) 再将一个个小任务运算的结果进行 join 汇总

image-20221009111101244

早在 jdk 1.7 这种 fork/join 框架处理问题的方式就已经诞生 但是当时使用这种方式处理问题 较为的复杂 应用不是非常广泛 到 jkd 1.8 才真正简化了其中的应用过程

fork/join 框架 与 多线程 的区别

传统多线程 是将 多个线程任务 通过算法合理分配给 CPU 的多个内核去完成计算 传统多线程 会出现阻塞等一系列问题 通过锁 等机制来进行优化 导致存在问题 一旦一个内核上的线程存在 长时间阻塞 而其他内核完成工作 处于空闲状态 当前内核的 阻塞任务 不仅阻塞了当前内核上正在运行的线程 也导致 其他线程处于排队状态 而此时的其他内核又处于空闲状态 从而造成资源浪费

使用 fork/join 框架

fork/join 框架采用 “工作窃取” 模式 (work - stealing)

当执行新的任务时 它可以将其拆分成更小的任务执行 并将小任务加载到线程队列中 如果当前 内核的线程队列中没有线程任务 它就会从一个随机线程的队列中偷一个将其放到自己队列中 –此线程队列是双端队列

相较于一般的线程池实现 fork/join 框架的优势体现在 对其中包含任务的处理方式上

在一般的线程池中 如果一个线程正在执行任务 由于某些原因 (线程任务队列耗尽) 无法继续运行 那么该线程会处于等待装填 而在 fork/join 框架 实现中 如果因为 某个子问题等待另一个子问题的完成 而无法继续运行 那么处理该子问题的线程 就会主动寻找其他尚未运行的子问题来执行 通过这种方式减少了线程的等待时间 提高的并行工作效率 从而提高了性能

并行流就是将一个内容 分成多个数据块 并用不同的线程分别处理每个数据块的流

Java8中 对其进行了优化 使开发者可以很容易的对数据进行并行流操作 StreamAPI 可以声明性的通过 parallel()sequential()

在并行流与顺序流中 进行切换

参考代码

Demo01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package Java8.ForkJoinWorkMode;

import java.util.concurrent.RecursiveTask;

public class Demo01 extends RecursiveTask<Long> {

// 实现 序列化
private static final long serialVersionUID = -2723274113476143585L;

private long start;
private long end;

private static final long THRESHOLD = 10000;

public Demo01(long start, long end) {
this.start = start;
this.end = end;
}

@Override
protected Long compute() {
long length = end - start;

if (length <= THRESHOLD) {
long sum = 0;

for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
long middle = (start + end) / 2;

Demo01 left = new Demo01(start, middle);
left.fork(); // 拆分子任务 同时 压入线程队列

Demo01 right = new Demo01(middle + 1, end);
right.fork();

return left.join() + right.join();
}
}
}

MyTest01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package Java8.ForkJoinWorkMode;

import Java8.ForkJoinWorkMode.Demo01;
import org.junit.Test;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;

public class MyTest01 {

// 传统使用 并行流 方式
@Test
public void test01() { // 33
Instant now1 = Instant.now();
long start = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> task = new Demo01(0, 100000000L);
Long sum = pool.invoke(task);
long end = System.currentTimeMillis();
Instant now2 = Instant.now();
long time = end - start;
// TODO 探究为什么创建 对象不消耗时间
System.out.println(sum);
System.out.println("It takes " + Duration.between(now1, now2).getNano());
System.out.println("It takes " + Duration.between(now1, now2).toMillis());
System.out.println(time);
}

// 对比 不使用 FORK JOIN 框架
@Test
public void test02() { // 410
Instant now1 = Instant.now();
Long sum = 0L;
for (int i = 1; i <= 100000000L; i++) {
sum += i;
}
Instant now2 = Instant.now();
System.out.println(sum);
System.out.println("It takes " + Duration.between(now1, now2).toMillis());
}

// Java8 使用并行流 方式
@Test
public void test03() {
Instant now1 = Instant.now();
long sum = LongStream.rangeClosed(0, 100000000L)
.parallel()
.reduce(0, Long::sum);
Instant now2 = Instant.now();
System.out.println(sum);
System.out.println("It takes " + Duration.between(now1, now2).toMillis());
}
}

5. Optional 类

Optional<T> 类 (java.util.Optional) 是一个容器类 代表一个值存在 或 不存在 原来使用 null 表示一个值不存在 现在使用 Optional 可以更好的表达这个概念 并且可以避免空指针异常

常用方法:

  • Optional.of(T t) 创建一个 Optional 实例
  • Optional.empty() 创建一个空的 Optional 实例
  • Optional.ofNullable(T t) 若 t 不为 null 则创建 Optional 实例 否则创建空实例
  • isPresent() 判断是否包含值
  • orElse(T t) 如果调用对象包含值 则返回该值 否则返回 t
  • orElseGet(Supplier s) 如果调用对象包含值 则返回该值 否则返回 s 获取的值
  • map(Function f) 如果有值 则对其处理 并返回处理后的 Optional 否则 返回 Optional.empty()
  • flatMap(Function mapper) 与 map 类似 要求 返回值必须是 Optional

参考代码

Demo01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package Optional;

import lambda.pojo.Employee;
import org.junit.Test;

import java.util.Optional;

public class Demo01 {
/*
+ `Optional.of(T t)` 创建一个 Optional 实例
+ `Optional.empty()` 创建一个空的 Optional 实例
+ `Optional.ofNullable(T t)` 若 t 不为 null 则创建 Optional 实例 否则创建空实例
+ `isPresent()` 判断是否包含值
+ `orElse(T t)` 如果调用对象包含值 则返回该值 否则返回 t
+ `orElseGet(Supplier s)` 如果调用对象包含值 则返回该值 否则返回 s 获取的值
+ `map(Function f)` 如果有值 则对其处理 并返回处理后的 Optional 否则 返回 `Optional.empty()`
+ `flatMap(Function mapper)` 与 map 类似 要求 返回值必须是 Optional
*/

@Test
public void test01() {
Optional<Employee> op = Optional.of(new Employee());
Employee emp = op.get();
System.out.println(emp);
}

@Test
public void test02() {
Optional<Employee> op = Optional.empty();
System.out.println(op.get());
}

@Test
public void test03() {
Optional<Employee> op = Optional.ofNullable(null);
if (op.isPresent()) {
System.out.println(op.get());
}
Employee emoZhangsan = op.orElse(new Employee("张三", 19, 9000, Employee.Status.VOCATION));
Employee empLisi = op.orElseGet(() -> {
System.out.println("hanshushijiekou");
return new Employee("李四", 14, 8000, Employee.Status.FREE);
});
// System.out.println(op.get());
System.out.println(emoZhangsan);
System.out.println(empLisi);
}

@Test
public void test04() {
Optional<Employee> op = Optional.of(new Employee("张三", 19, 3000, Employee.Status.VOCATION));
Optional<String> ret = op.map(Employee::getName);
if (ret.isPresent()) {
System.out.println(ret.get());
}
// 代码优化后
ret.ifPresent(System.out::println);
// 进一步防止 NPE
Optional<String> ret2 = op.flatMap(e -> Optional.of(e.getName()));
System.out.println(ret2.get());
}
}

6. 接口的默认方法与静态方法

默认方法

在Java8 之前 接口中只允许存在 常量抽象方法

Java8 后 允许定义 使用 default 修饰的默认方法 而默认方法不是抽象方法 不强制要求重写 但是重写时 必须去掉 default 关键字

编写默认方法时 注意 public 可以省略 但 default 不能省略

注意 如果有多个接口 接口中存在相同的方法声明 那么子类 必须重写此方法

注意 接口默认方法的 “类优先” 原则

若一个接口定义了一个默认方法 而另一个 父类 或 接口 中又定义了一个同名的方法时 –==多接口 继承 冲突问题==

  • 选择父类方法 –==一个父类 与 一个接口== 提供了 相同名称 参数列表的方法 如果==父类==提供了具体的实现 那么接口中具有相同名称和参数的默认方法会被==忽略==
  • 接口冲突 (接口多实现) –如果同时==两个接口的方法== 相同名称 相同参数列表 (不管方法是否为默认方法) 都必须覆盖该接口 ==重写方法==来解决冲突问题
1
2
3
4
5
6
7
package Optional;

public interface MyInterface {
default String getName() {
return "hahaha";
}
}

静态方法

同时 在 Java8 后允许定义 static 静态方法 使用 static 关键字创建

注意:使用 static 创建的静态方法 只能通过接口名调用 不能通过 类名 或 对象名 调用 同样 创建时 public 可省略 static 不可省略

1
2
3
4
5
6
7
8
9
10
11
package Optional;

public interface MyInterface {
default String getName() {
return "hahaha";
}

static void show() {
System.out.println("static hahaha");
}
}

7. 新时间日期 API

传统时间API

Date

Calendar

TimeZone

SimpleDateFormat

Java8 新时间日期API

使用 LocalDate LocalTime LocalDateTIme 三种新 Java8 提供的时间日期API

  • LocalDate LocalTime LocalDateTIme 类的实例 是不可变的对象 分别表示使用 ISO-8601 日历系统的日期 时间 日期和时间 它们提供简单的日期或时间 并不包含当前的时间信息 也不包含于时区相关的信息
    ISO-8601 日历系统是国际化标准组织制定的现代公民的日期和时间标识法

参考代码

DateFormatThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package Date;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateFormatThreadLocal {

private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};

public static Date convert(String source) throws ParseException {
return df.get().parse(source);
}
}

TestSimpleDateFormat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package Date;

import org.junit.Test;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
/*
传统时间 API 使用 锁机制 解决 时间API 线程不安全问题
*/
public class TestSimpleDateFormat {

// 传统时间 API 不使用 锁机制 导致线程安全问题
@Test
public void test01() throws ExecutionException, InterruptedException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
Callable<Date> tast = new Callable<Date>() {
@Override
public Date call() throws Exception {
return sdf.parse("20221224");
}
};
ExecutorService pool = Executors.newFixedThreadPool(10);
List<Future<Date>> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
results.add(pool.submit(tast));
}
for (Future<Date> result : results) {
System.out.println(result.get());
}
}

// 传统时间 API 使用锁机制 解决线程不安全问题
@Test
public void test02() throws ExecutionException, InterruptedException {
Callable<Date> tast = new Callable<Date>() {
@Override
public Date call() throws Exception {
return DateFormatThreadLocal.convert("20221224");
}
};
ExecutorService pool = Executors.newFixedThreadPool(10);
List<Future<Date>> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
results.add(pool.submit(tast));
}
for (Future<Date> result : results) {
System.out.println(result.get());
}
pool.shutdown();
}

// Java8 新时间 API 处理 线程不安全问题 --不存在线程安全问题 因为 Java8 中对时间API的操作都会创建新的实例
@Test
public void test03() throws ExecutionException, InterruptedException {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
Callable<LocalDate> tast = new Callable<LocalDate>() {
@Override
public LocalDate call() throws Exception {
return LocalDate.parse("20221224", dtf);
}
};
ExecutorService pool = Executors.newFixedThreadPool(10);
List<Future<LocalDate>> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
results.add(pool.submit(tast));
}
for (Future<LocalDate> result : results) {
System.out.println(result.get());
}
pool.shutdown();
}
}

TestLocalDateTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package Date;

import org.junit.Test;

import java.time.*;

public class TestLocalDateTime {

// LocalDate LocalTime LocalDateTIme
@Test
public void test01() {
LocalDateTime time = LocalDateTime.now();
System.out.println(time);

LocalDateTime time2 = LocalDateTime.of(2024, 6, 30, 9, 30, 13, 140000);
System.out.println(time2);

// 产生新的实例 --处理线程安全问题
LocalDateTime time3 = time.plusYears(2);
System.out.println(time3);

LocalDateTime time4 = time.minusMonths(2);
System.out.println(time4);

System.out.println(time4.getDayOfMonth());
System.out.println(time4.getDayOfWeek());
System.out.println(time4.getDayOfYear());
}

// Instant: 时间戳 (以 Unix 元年: 1970年1月1日 00:00:00 到 某个时间 --or now 为止的毫秒值)
@Test
public void test02() {
Instant now = Instant.now(); // 默认获取 UTC 时区
System.out.println(now);

// 获取 东8区 时间
OffsetDateTime time = now.atOffset(ZoneOffset.ofHours(8));
System.out.println(time);

// 转成毫秒值 时间戳
System.out.println(now.toEpochMilli());

// 将 毫秒时间戳 转换成 Unix 元年时间
Instant time2 = Instant.ofEpochSecond(10000);
System.out.println(time2);
}

/*
Duration: 计算两个 "时间" 的间隔
Period: 计算两个 "日期" 的
*/
@Test
public void test03() {
Instant now1 = Instant.now();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instant now2 = Instant.now();
Duration duration = Duration.between(now1, now2);

System.out.println(duration.toMillis());
System.out.println("-----------------------------");

LocalTime now3 = LocalTime.now();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

LocalTime now4 = LocalTime.now();

System.out.println(Duration.between(now3, now4).toMillis());
}

@Test
public void test04() {
LocalDate now = LocalDate.of(2015,1,1);
LocalDate now2 = LocalDate.of(2016,1,1);
Period period = Period.between(now, now2);
System.out.println(period.toTotalMonths());

System.out.println("-------------------------");
// 需求 --计算 两个日期时间之间的天数 --Java8新特性 牛批
LocalDateTime time = LocalDateTime.of(2015, 1, 1, 0, 0);
LocalDateTime time2 = LocalDateTime.of(2019, 1, 1, 0, 0);
Duration duration = Duration.between(time, time2);
System.out.println(duration.toDays());
}
}

操纵日期

时间日期校正器

  • TemporalAdjuster: 时间校正器 在开发人员 需要获取例如: 将日期 校正调整到 “下个周日” 等操作
  • TemporalAdjusters: 该类 通过静态方法 提供了大量的常用 TemporalAdjuster 的实现

时间日期格式化

参考代码

时区处理

Java8 中加入了对时区的支持 带时区的时间分别为

  • ZonedDate
  • ZonedTime
  • ZonedDateTime

每个时区都对应着 ID 地区ID都为 “{区域}/{城市}” 的格式

例如 Asia/Shanghai 等

ZoneId 该类中包含了所有的时区信息

  • getAvailableZoneIds() 可以获取所有时区信息
  • of() 用指定时区信息 获取 ZoneId 对象

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package Date;

import org.junit.Test;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TestTemporalAdjuster {

// TemporalAdjuster 时间校准器
@Test
public void test01() {
LocalDate nextSunday = LocalDate.now().with(
TemporalAdjusters.next(DayOfWeek.SUNDAY)
);
System.out.println(nextSunday);

System.out.println("---------------------");

LocalDateTime now = LocalDateTime.now();
System.out.println(now);
LocalDateTime time = now.withDayOfMonth(10);
System.out.println(time);

System.out.println("---------------------");

LocalDateTime time2 = now.with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY));
System.out.println(time2);

System.out.println("---------------------");

// 需求 获取 下一个工作日
LocalDateTime time4 = now.with(l -> {
LocalDateTime time3 = (LocalDateTime) l;
DayOfWeek dayOfWeek = time3.getDayOfWeek();
if (dayOfWeek.equals(DayOfWeek.FRIDAY)) {
return time3.plusDays(3);
} else if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {
return time3.plusDays(2);
} else {
return time3.plusDays(1);
}
});

System.out.println(time4);
}

// DateTimeFormatter 格式化 时间 日期
@Test
public void test02() {
DateTimeFormatter time = DateTimeFormatter.ISO_DATE_TIME;
LocalDateTime now = LocalDateTime.now();
String newTime = now.format(time);
System.out.println(newTime);

System.out.println("------------------");

DateTimeFormatter time2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String newTime2 = now.format(time2);
System.out.println(newTime2);

System.out.println("------------------");

LocalDateTime newDate3 = now.parse(newTime2, time2);
System.out.println(newDate3);
}

// 时区处理 + ZonedDate + ZonedTime + ZonedDateTime
@Test
public void test03() {
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
zoneIds.forEach(System.out::println);

System.out.println("----------------");

LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Aden"));
System.out.println(now);

LocalDateTime now2 = LocalDateTime.now();
LocalDateTime now3 = LocalDateTime.now(ZoneId.of("Asia/Aden"));
ZonedDateTime time = now2.atZone(ZoneId.of("Asia/Aden"));
ZonedDateTime time2 = now3.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(time);
System.out.println(time2);

}
}

8. 重复注解 与 类型注解

重复注解 与 类型注解

参考代码

MyAnnotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package Java8.Anno;

import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;

@Repeatable(MyAnnotations.class)
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value() default "line";
}

MyAnnotations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Java8.Anno;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations {
MyAnnotation[] value();
}

TestAnnotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package Java8.Anno;

import org.junit.Test;

import java.lang.reflect.Method;

/**
* 重复注解 与 类型注解
*/
public class TestAnnotation {

// check framework
private /*@NotNull*/ Object obj = null;

@Test
public void test01() throws NoSuchMethodException {
Class<TestAnnotation> clazz = TestAnnotation.class;
Method method = clazz.getMethod("show");

MyAnnotation[] ma = method.getAnnotationsByType(MyAnnotation.class);
for (MyAnnotation myAnnotation : ma) {
System.out.println(myAnnotation.value());
}
}

@MyAnnotation("Hello")
@MyAnnotation("33")
public void show(@MyAnnotation("abc") String str) {

}
}

9. 其他新特性

….

Vue_Note

1. 前端核心分析

前端四要素:

  1. 逻辑

  2. 事件

  3. 视图 CSS难点 –>BootStrap 可视化布局

    Soc 关注点分离离乱 Separation of concerns

    HTML+CSS+JS 视图 –> 用户交互页面 刷新后台数据

  4. 通信

    网络通信 axios

1.1 Vue概述

Soc原则:关注点分离原则

Vue 的核心库只关注视图层,方便与第三方库或既有项目整合。

HTML + CSS + JS : 视图 : 给用户看,刷新后台给的数据

网络通信 : axios

页面跳转 : vue-router

状态管理:vuex

Vue-UI : ICE , Element UI

前端项目构建 管理打包工具 ==webpack==

AngularJS将MVC模型搬到了前端 形成了MVVC模型 模块化模式 vm:数据双向绑定

Data:

vm:数据双向绑定

虚拟Dom 利用内存 计算属性–>Vue特色

==Vue集大成者==

1.2 前端三要素

  • HTML(结构):超文本标记语言(Hyper Text Markup Language),决定网页的结构和内容
  • CSS(表现):层叠样式表(Cascading Style Sheets),设定网页的表现样式。
  • JavaScript(行为):是一种弱类型脚本语言,其源码不需经过编译,而是由浏览器解释运行,用于控制网页的行为

结构层(HTML)

表现层(CSS)

CSS层叠样式表是一门标记语言,并不是编程语言,因此不可以自定义变量,不可以引用等,换句话说就是不具备任何语法支持,它主要缺陷如下:

  • 语法不够强大,比如无法嵌套书写,导致模块化开发中需要书写很多重复的选择器;
  • 没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,导致难以维护;这就导致了我们在工作中无端增加了许多工作量。为了解决这个问题,前端开发人员会使用一种称之为【CSS预处理器】的工具,提供CSS缺失的样式层复用机制、减少冗余代码,提高样式代码的可维护性。大大的提高了前端在样式上的开发效率。

什么是==CSS预处理器==:

CSS预处理器定义了一种新的语言,其基本思想是,用一种专门的编程语言,为CSS增加了一些编程的特性,将CSS作为目标生成文件,然后开发者就只需要使用这种语言进行CSS的编码工作。转化成通俗易懂的话来说就是“用一种专门的编程语言,进行Web页面样式设计,再通过编译器转化为正常的CSS文件,以供项目使用”。

常用的CSS预处理器有哪些

  • SASS:基于Ruby ,通过服务端处理,功能强大。解析效率高。需要学习Ruby语言,上手难度高于LESS。
  • LESS:基于NodeJS,通过客户端处理,使用简单。功能比SASS简单,解析效率也低于SASS,但在实际开发中足够了,所以如果我们后台人员如果需要的话,建议使用LESS。

行为层(JavaScript)

JavaScript一门弱类型脚本语言,其源代码在发往客户端运行之前不需要经过编译,而是将文本格式的字符代码发送给浏览器,由浏览器解释运行。

Native 原生JS开发

原生JS开发,也就是让我们按照【ECMAScript】标准的开发方式,简称ES,特点是所有浏览器都支持。截至到当前,ES标准以发布如下版本:

  • ES3
  • ES4(内部,未正式发布)
  • ES5(全浏览器支持)
  • ES6(常用,当前主流版本:webpack打包成为ES5支持)
  • ES7
  • ES8
  • ES9(草案阶段)

区别就是逐步增加新特性。

TypeScript 微软的标准

TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集, 而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。由安德斯·海尔斯伯格(C#、Delphi、TypeScript之父; .NET创立者) 主导。该语言的特点就是除了具备ES的特性之外还纳入了许多不在标准范围内的新特性,所以会导致很多浏览器不能直接支持TypeScript语法, 需要编译后(编译成JS) 才能被浏览器正确执行。

JavaScript框架

  • **==JQuery==**:大家熟知的JavaScript库,优点就是简化了DOM操作,缺点就是DOM操作太频繁,影响前端性能;在前端眼里使用它仅仅是为了兼容IE6,7,8;
  • ==Angular==:Google收购的前端框架,由一群Java程序员开发,其特点是将后台的MVC模式搬到了前端并增加了模块化开发的理念,与微软合作,采用了TypeScript语法开发;对后台程序员友好,对前端程序员不太友好;最大的缺点是版本迭代不合理(如1代–>2 代,除了名字,基本就是两个东西;截止发表博客时已推出了Angular6)
  • ==React==:Facebook 出品,一款高性能的JS前端框架;特点是提出了新概念 【虚拟DOM】用于减少真实 DOM 操作,在内存中模拟 DOM操作,有效的提升了前端渲染效率;缺点是使用复杂,因为需要额外学习一门【JSX】语言;
  • **==Vue==**:一款渐进式 JavaScript 框架,所谓渐进式就是逐步实现新特性的意思,如实现模块化开发、路由、状态管理等新特性。其特点是综合了 Angular(模块化)和React(虚拟 DOM) 的优点;
  • **==Axios==**:前端通信框架;因为 Vue 的边界很明确,就是为了处理 DOM,所以并不具备通信能力,此时就需要额外使用一个通信框架与服务器交互;当然也可以直接选择使用jQuery 提供的AJAX 通信功能;

2. 前端发展

2.1 UI框架

  • Ant-Design:阿里巴巴出品,基于React的UI框架
  • ElementUI、iview、ice:饿了么出品,基于Vue的UI框架
  • BootStrap:Teitter推出的一个用于前端开发的开源工具包
  • AmazeUI:又叫“妹子UI”,一款HTML5跨屏前端框架

2.2 JavaScript构建工具

  • Babel:JS编译工具,主要用于浏览器不支持的ES新特性,比如用于编译TypeScript

  • WebPack:模块打包器,主要作用就是打包、压缩、合并及按序加载

    注:以上知识点已将WebApp开发所需技能全部梳理完毕

2.3 三端同一

混合开发(Hybrid App)

主要目的是实现一套代码三端统一(PC、Android:.apk、iOS:.ipa)并能够调用到设备底层硬件(如:传感器、GPS、摄像头等),打包方式主要有以下两种:

  • 云打包:HBuild -> HBuildX,DCloud 出品;API Cloud
  • 本地打包: Cordova(前身是 PhoneGap)

微信小程序

详见微信官网,这里就是介绍一个方便微信小程序UI开发的框架:WeUI

2.4 后端技术

前端人员为了方便开发也需要掌握一定的后端技术但我们Java后台人员知道后台知识体系极其庞大复杂,所以为了方便前端人员开发后台应用,就出现了Node JS这样的技术。Node JS的作者已经声称放弃Node JS(说是架构做的不好再加上笨重的node modules,可能让作者不爽了吧)开始开发全新架构的Deno
既然是后台技术,那肯定也需要框架和项目管理工具, Node JS框架及项目管理工具如下:

  • Express:Node JS框架
  • Koa:Express简化版
  • NPM:项目综合管理工具,类似于Maven
  • YARN:NPM的替代方案,类似于Maven和Gradle的关系

2.5 主流前端框架

Vue.js

iView

iview是一个强大的基于Vue的UI库, 有很多实用的基础组件比element ui的组件更丰富, 主要服务于PC界面的中后台产品。使用单文件的Vue组件化开发模式基于npm+webpack+babel开发, 支持ES 2015高质量、功能丰富友好的API, 自由灵活地使用空间。

  • 官网地址
  • Github
  • iview-admin

备注:属于前端主流框架,选型时可考虑使用,主要特点是移动端支持较多

Element UI

Element是饿了么前端开源维护的Vue UI组件库, 组件齐全, 基本涵盖后台所需的所有组件,文档讲解详细, 例子也很丰富。主要用于开发PC端的页面, 是一个质量比较高的Vue UI组件库。
·官网地址
·Git hub
·vue-element-admin
备注:属于前端主流框架,选型时可考虑使用,主要特点是桌面端支持较多

ICE

飞冰是阿里巴巴团队基于React/Angular/Vue的中后台应用解决方案, 在阿里巴巴内部, 已经有270多个来自几乎所有BU的项目在使用。飞冰包含了一条从设计端到开发端的完整链路,帮助用户快速搭建属于自己的中后台应用。

  • 官网地址。
  • Git hub

备注:主要组件还是以React为主, 截止2019年02月17日更新博客前对Vue的支持还不太完善,目前尚处于观望阶段

VantUI

Vant UI是有赞前端团队基于有赞统一的规范实现的Vue组件库, 提供了-整套UI基础组件和业务组件。通过Vant, 可以快速搭建出风格统一的页面,提升开发效率。

官网地址
Github

AtUI

at-ui是一款基于Vue 2.x的前端UI组件库, 主要用于快速开发PC网站产品。它提供了一套n pm+web pack+babel前端开发工作流程, CSS样式独立, 即使采用不同的框架实现都能保持统一的UI风格。
·官网地址
·Git hub

Cube Ul

cube-ui是滴滴团队开发的基于Vue js实现的精致移动端组件库。支持按需引入和后编译, 轻量灵活;扩展性强,可以方便地基于现有组件实现二次开发。

  • 官网地址
  • Github

混合开发

Flutter

Flutter是谷歌的移动端UI框架, 可在极短的时间内构建Android和iOS上高质量的原生级应用。Flutter可与现有代码一起工作, 它被世界各地的开发者和组织使用, 并且Flutter是免费和开源的。

  • 官网地址

Github
备注:Google出品, 主要特点是快速构建原生APP应用程序, 如做混合应用该框架为必选框架

lonic

lonic既是一个CSS框架也是一个Javascript UI库, lonic是目前最有潜力的一款HTML 5手机应用开发框架。通过SASS构建应用程序, 它提供了很多UI组件来帮助开发者开发强大的应用。它使用JavaScript MV VM框架和Angular JS/Vue来增强应用。提供数据的双向绑定, 使用它成为Web和移动开发者的共同选择。

官网地址
·官网文档
·Git hub

  • 微信小程序
  • mpvue

mpvue是美团开发的一个使用Vue.js开发小程序的前端框架, 目前支持微信小程序、百度智能小程序,头条小程序和支付宝小程序。框架基于Vue.js, 修改了的运行时框架runtime和代码编译器compiler实现, 使其可运行在小程序环境中, 从而为小程序开发引入了Vue.js开发体验。
·官网地址
·Git hub
备注:完备的Vue开发体验, 井且支持多平台的小程序开发, 推荐使用

WeUI

WeUI是一套同微信原生视觉体验一致的基础样式库, 由微信官方设计团队为微信内网页和微信小程序量身设计, 令用户的使用感知更加统一。包含button、cell、dialog、toast、article、icon等各式元素。

  • 官网地址
  • Github

==为什么需要前后分离?==

  1. 后端为主的MVC时代

为了降低开发的复杂度, 以后端为出发点, 比如:Struts、Spring MVC等框架的使用, 就是后端的MVC时代;
以SpringMVC流程为例:

image-20220807112827186

  1. 发起请求到前端控制器(Dispatcher Servlet)
  2. 前端控制器请求HandlerMapping查找Handler,可以根据xml配置、注解进行查找
  3. 处理器映射器HandlerMapping向前端控制器返回Handler
  4. 前端控制器调用处理器适配器去执行Handler
  5. 处理器适配器去执行Handler
  6. Handler执行完成给适配器返回ModelAndView
  7. 处理器适配器向前端控制器返回ModelAndView,ModelAndView是SpringMvc框架的一个底层对象,包括Model和View
  8. 前端控制器请求视图解析器去进行视图解析,根据逻辑视图名解析成真正的视图(JSP)
  9. 视图解析器向前端控制器返回View
  10. 前端控制器进行视图渲染,视图渲染将模型数据(在ModelAndView对象中)填充到request域
  11. 前端控制器向用户响应结果

优点

  • MVC是一个非常好的协作模式, 能够有效降低代码的耦合度从架构上能够让开发者明白代码应该写在哪里。为了让View更纯粹, 还可以使用Thyme leaf、Frree marker等模板引擎, 使模板里无法写入Java代码, 让前后端分工更加清晰。

缺点

  • 前端开发重度依赖开发环境,开发效率低,这种架构下,前后端协作有两种模式:
  • 第一种是前端写DEMO, 写好后, 让后端去套模板。好处是DEMO可以本地开发, 很高效。不足是还需要后端套模板,有可能套错,套完后还需要前端确定,来回沟通调整的成本比较大;
  • 另一种协作模式是前端负责浏览器端的所有开发和服务器端的View层模板开发。好处是UI相关的代码都是前端去写就好,后端不用太关注,不足就是前端开发重度绑定后端环境,环境成为影响前端开发效率的重要因素。
  • 前后端职责纠缠不清:模板引擎功能强大,依旧可以通过拿到的上下文变量来实现各种业务逻辑。这样,只要前端弱势一点,往往就会被后端要求在模板层写出不少业务代码,还有一个很大的灰色地带是Controller, 页面路由等功能本应该是前端最关注的, 但却是由后端来实现。Controller本身与Model往往也会纠缠不清,看了让人咬牙的业务代码经常会出现在Controller层。这些问题不能全归结于程序员的素养, 否则JSP就够了。
  • 对前端发挥的局限性:性能优化如果只在前端做空间非常有限,于是我们经常需要后端合作,但由于后端框架限制,我们很难使用[Comet】、【Big Pipe】等技术方案来优化性能。

注:在这期间(2005年以前) , 包括早期的JSP、PHP可以称之为Web 1.0时代。在这里想说一句, 如果你是一名Java初学者, 请你不要再把一些陈旧的技术当回事了, 比如JSP, 因为时代在变、技术在变、什么都在变(引用扎克伯格的一句话:唯一不变的是变化本身);当我们去给大学做实训时,有些同学会认为我们没有讲什么干货,其实不然,只能说是你认知里的干货对于市场来说早就过时了而已

  1. 基于AJAX带来的SPA时代

时间回到2005年A OAX(Asynchronous JavaScript And XML, 异步JavaScript和XML,老技术新用法)被正式提出并开始使用CDN作为静态资源存储, 于是出现了JavaScript王者归来(在这之前JS都是用来在网页上贴狗皮膏药广告的) 的SPA(Single Page Application) 单页面应用时代。

image-20220807121017647

优点

  • 这种模式下, 前后端的分工非常清晰, 前后端的关键协作点是AJAX接口。看起来是如此美妙, 但回过头来看看的话, 这与JSP时代区别不大。复杂度从服务端的JSP里移到了浏览器的JavaScript,浏览器端变得很复杂。类似Spring MVC, 这个时代开始出现浏览器端的分层架构

image-20220807121336544

缺点

  • 前后端接口的约定:如果后端的接口一塌糊涂,如果后端的业务模型不够稳定,那么前端开发会很痛苦;不少团队也有类似尝试,通过接口规则、接口平台等方式来做。有了和后端一起沉淀的接口规则,还可以用来模拟数据,使得前后端可以在约定接口后实现高效并行开发。
  • 前端开发的复杂度控制:SPA应用大多以功能交互型为主,JavaScript代码过十万行很正常。大量JS代码的组织,与View层的绑定等,都不是容易的事情。
  1. 前端为主的MV*时代

此处的MV*模式如下:

  • MVC(同步通信为主) :Model、View、Controller
  • MVP(异步通信为主) :Model、View、Presenter
  • MVVM(异步通信为主):Model、View、View Model

为了降低前端开发复杂度,涌现了大量的前端框架,比如:Angular JS –前端模块化开发MVC模式、React –虚拟dom 将dom放在内存中使用 不用经过浏览器、Vue.js、Ember JS等, 这些框架总的原则是先按类型分层, 比如Templates、Controllers、Models, 然后再在层内做切分,如下图:

image-20220807124120793

优点

  • 前后端职责很清晰:前端工作在浏览器端,后端工作在服务端。清晰的分工,可以让开发并行,测试数据的模拟不难, 前端可以本地开发。后端则可以专注于业务逻辑的处理, 输出RESTful等接口。
  • 前端开发的复杂度可控:前端代码很重,但合理的分层,让前端代码能各司其职。这一块蛮有意思的,简单如模板特性的选择,就有很多很多讲究。并非越强大越好,限制什么,留下哪些自由,代码应该如何组织,所有这一切设计,得花一本书的厚度去说明。
  • 部署相对独立:可以快速改进产品体验

缺点

  • 代码不能复用。比如后端依旧需要对数据做各种校验,校验逻辑无法复用浏览器端的代码。如果可以复用,那么后端的数据校验可以相对简单化。
  • 全异步, 对SEO(广告搜索优化)不利。往往还需要服务端做同步渲染的降级方案。
  • 性能并非最佳,特别是移动互联网环境下。
  • SPA不能满足所有需求, 依旧存在大量多页面应用。URL Design需要后端配合, 前端无法完全掌控。
  1. Node JS带来的全栈时代

前端为主的MV*模式解决了很多很多问题, 但如上所述, 依旧存在不少不足之处。随着Node JS的兴起, JavaScript开始有能力运行在服务端。这意味着可以有一种新的研发模式:

image-20220807124432247

在这种研发模式下,前后端的职责很清晰。对前端来说,两个UI层各司其职:

  • Front-end Ul layer处理浏览器层的展现逻辑。通过CSS渲染样式, 通过JavaScript添加交互功能, HTML的生成也可以放在这层, 具体看应用场景。
  • Back-end Ul layer处理路由、模板、数据获取、Cookie等。通过路由, 前端终于可以自主把控URL Design, 这样无论是单页面应用还是多页面应用, 前端都可以自由调控。后端也终于可以摆脱对展现的强关注,转而可以专心于业务逻辑层的开发。

通过Node, WebServer层也是JavaScript代码, 这意味着部分代码可前后复用, 需要SEO的场景可以在服务端同步渲染,由于异步请求太多导致的性能问题也可以通过服务端来缓解。前一种模式的不足,通过这种模式几乎都能完美解决掉。

与JSP模式相比, 全栈模式看起来是一种回归, 也的确是一种向原始开发模式的回归, 不过是一种螺旋上升式的回归。

基于Node JS的全栈模式, 依旧面临很多挑战:

  • 需要前端对服务端编程有更进一步的认识。比如TCP/IP等网络知识的掌握。
  • Node JS层与Java层的高效通信。Node JS模式下, 都在服务器端, RESTful HTTP通信未必高效, 通过SOAP等方式通信更高效。一切需要在验证中前行。
  • 对部署、运维层面的熟练了解,需要更多知识点和实操经验。
  • 大量历史遗留问题如何过渡。这可能是最大最大的阻力。

注:看到这里,相信很多同学就可以理解,为什么我总在课堂上说:“前端想学后台很难,而我们后端程序员学任何东西都很简单”;就是因为我们后端程序员具备相对完善的知识体系。

全栈!So Easy!

==总结:==

综上所述,模式也好,技术也罢,没有好坏优劣之分,只有适合不适合;前后分离的开发思想主要是基于Soc(关注度分离原则),上面种种模式,都是让前后端的职责更清晰,分工更合理高效

3. 实操Vue基础项目

什么是MVVM

MVVM(Model-View-ViewModel)是一种软件设计模式,由微软WPF(用于替代WinForm,以前就是用这个技术开发桌面应用程序的)和Silverlight(类似于Java Applet,简单点说就是在浏览器上运行WPF)的架构师Ken Cooper和Ted Peters开发,是一种简化用户界面的事件驱动编程方式。由John Gossman(同样也是WPF和Sliverlight的架构师)与2005年在他的博客上发表。

MVVM源自于经典的MVC(Model-View-Controller)模式。MVVM的核心是ViewModel层,负责转换Model中的数据对象来让数据变得更容易管理和使用。其作用如下:

  • 该层向上与视图层进行双向数据绑定
  • 向下与Model层通过接口请求进行数据交互

image-20220807141448927

MVVM已经相当成熟了,主要运用但不仅仅在网络应用程序开发中。当下流行的MVVM框架有Vue.js,Anfular JS

为什么要使用MVVM

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大好处

  • 低耦合:视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的View上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
  • 可复用:你可以把一些视图逻辑放在一个ViewModel里面,让很多View重用这段视图逻辑。
  • 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewMode),设计人员可以专注于页面设计。
  • 可测试:界面素来是比较难以测试的,而现在测试可以针对ViewModel来写。

MVVM的组成部分

image-20220807141635610

(1)View

View是视图层, 也就是用户界面。前端主要由HTH L和csS来构建, 为了更方便地展现vi eu to del或者Hodel层的数据, 已经产生了各种各样的前后端模板语言, 比如FreeMarker,Thyme leaf等等, 各大MV VM框架如Vue.js.Angular JS, EJS等也都有自己用来构建用户界面的内置模板语言。

(2)Model

Model是指数据模型, 泛指后端进行的各种业务逻辑处理和数据操控, 主要围绕数据库系统展开。这里的难点主要在于需要和前端约定统一的接口规则

(3)ViewModel

ViewModel是由前端开发人员组织生成和维护的视图数据层。在这一层, 前端开发者对从后端获取的Model数据进行转换处理, 做二次封装, 以生成符合View层使用预期的视图数据模型。

需要注意的是View Model所封装出来的数据模型包括视图的状态和行为两部分, 而Model层的数据模型是只包含状态的

  • 比如页面的这一块展示什么,那一块展示什么这些都属于视图状态(展示)
  • 页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互)

视图状态和行为都封装在了View Model里。这样的封装使得View Model可以完整地去描述View层。由于实现了双向绑定, View Model的内容会实时展现在View层, 这是激动人心的, 因为前端开发者再也不必低效又麻烦地通过操纵DOM去更新视图。

MVVM框架已经把最脏最累的一块做好了, 我们开发者只需要处理和维护View Model, 更新数据视图就会自动得到相应更新,真正实现事件驱动编程。

View层展现的不是Model层的数据, 而是ViewModel的数据, 由ViewModel负责与Model层交互,获取和更新数据, 这就==完全解耦了View层和Model层, 这个解耦是至关重要的, 它是前后端分离方案实施的重要一环==。

Vue

Vue(读音/vju/, 类似于view) 是一套用于构建用户界面的渐进式框架, 发布于2014年2月。与其它大型框架不同的是, Vue被设计为可以自底向上逐层应用。Vue的核心库只关注视图层, 不仅易于上手, 还便于与第三方库(如:vue-router,vue-resource,vue x) 或既有项目整合。

(1)MVVM模式的实现者

  • Model:模型层, 在这里表示JavaScript对象
  • View:视图层, 在这里表示DOM(HTML操作的元素)
  • ViewModel:连接视图和数据的中间件, Vue.js就是MVVM中的View Model层的实现者

在MVVM架构中, 是不允许数据和视图直接通信的, 只能通过ViewModel来通信, 而View Model就是定义了一个Observer观察者

  • ViewModel能够观察到数据的变化, 并对视图对应的内容进行更新
  • ViewModel能够监听到视图的变化, 并能够通知数据发生改变

至此, 我们就明白了, Vue.js就是一个MV VM的实现者, 他的核心就是实现了DOM监听与数据绑定

(2)为什么要使用Vue.js

  • 轻量级, 体积小是一个重要指标。Vue.js压缩后有只有20多kb(Angular压缩后56kb+,React压缩后44kb+)
  • 移动优先。更适合移动端, 比如移动端的Touch事件
  • 易上手,学习曲线平稳,文档齐全
  • 吸取了Angular(模块化) 和React(虚拟DOM) 的长处, 并拥有自己独特的功能,如:计算属性
  • 开源,社区活跃度高

第一个Vue程序

【说明】IDEA可以安装Vue的插件!
  注意:Vue不支持IE 8及以下版本, 因为Vue使用了IE 8无法模拟的ECMAScript 5特性。但它支持所有兼容ECMAScript 5的浏览器。

(1)下载地址

开发版本

  • 包含完整的警告和调试模式:https://yuejs.org/js/vue.js
  • 删除了警告, 30.96KB min+gzip:https://vuejs.org/js/vue.min.js

CDN

(2)代码编写

Vue.js的核心是实现了MVVM模式, 她扮演的角色就是View Model层, 那么所谓的第一个应用程序就是展示她的数据绑定功能,操作流程如下:

  1. 创建一个HTML文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>

    </body>
    </html>
  2. 引入Vue.js

    1
    2
    3
    <!--1.导入Vue.js-->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
    12
  3. 创建一个Vue实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <script type="text/javascript">
    var vm = new Vue({
    el:"#app",
    /*Model:数据*/
    data:{
    message:"hello,vue!"
    }
    });
    </script>

    说明:

    • el: '#vue':绑定元素的ID
    • data:{message:'Hello Vue!'}:数据对象中有一个名为message的属性,并设置了初始值 Hello Vue!
  4. 将数据绑定到页面元素

    1
    2
    3
    4
    <!--view层,模板-->
    <div id="app">
    {{message}}
    </div>

(3)完整的HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>

</head>
<body>

<!--view层,模板-->
<div id="app">
{{message}}
</div>

<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
/*Model:数据*/
data:{
message:"hello,vue!"
}
});
</script>
</body>
</html>

(4)测试

为了能够更直观的体验Vue带来的数据绑定功能, 我们需要在浏览器测试一番, 操作流程如下:
  1、在浏览器上运行第一个Vue应用程序, 进入开发者工具
  2、在控制台输入vm.message=‘HelloWorld’, 然后回车, 你会发现浏览器中显示的内容会直接变成HelloWorld
  此时就可以在控制台直接输入vm.message来修改值, 中间是可以省略data的, 在这个操作中, 我并没有主动操作DOM, 就让页面的内容发生了变化, 这就是借助了Vue的数据绑定功能实现的; MV VM模式中要求View Model层就是使用观察者模式来实现数据的监听与绑定, 以做到数据与视图的快速响应。

4. Vue基础语法

Vue七大对象

  • el
    • 用来指示Vue 编译器从某个地方开始解析 Vue 语法 相当于一个占位符
  • data
    • 用来组织从 view 中抽象出来的属性 可以说将视图的数据抽象出来 存放在data中
  • template
    • 用来设置模板 会替换页面元素 包括占位符
  • methods
    • 放置在页面中的业务逻辑 js方法一般都放置在methods中
  • render
    • 创建真正的Virtual Dom
  • computed
    • 用来计算
  • watch
    • watch:function(new, old) {}
    • 监听data中的数据变化
    • 两个参数 一个返回新值 一个返回旧值

v-bind

我们已经成功创建了第一个Vue应用!看起来这跟渲染一个字符串模板非常类似, 但是Vue在背后做了大量工作。现在数据和DOM已经被建立了关联, 所有东西都是响应式的。我们在控制台操作对象属性,界面可以实时更新!
我们还可以使用v-bind来绑定元素特性!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en" xmlns:v-bind="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>

</head>
<body>

<!--view层,模板-->
<div id="app">
<span v-bind:title="message">
鼠标悬停几秒钟查看此处动态绑定的提示信息!
</span>
</div>

<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
/*Model:数据*/
data:{
message: '页面加载于 ' + new Date().toLocaleString()
}
});
</script>
</body>
</html>

你看到的v-bind等被称为指令指令带有前缀v以表示它们是Vue提供的特殊特性。可能你已经猜到了, 它们会在渲染的DOM上应用特殊的响应式行为在这里,该指令的意思是:“==将这个元素节点的title特性和Vue实例的message属性保持一致==”。

如果你再次打开浏览器的JavaScript控制台, 输入app, message=‘新消息’,就会再一次看到这个绑定了title特性的HTML已经进行了更新

v-if v-else

什么是条件判断语句,就不需要我说明了吧,以下两个属性!

v-if
v-else
上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en" xmlns:v-bind="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--view层,模板-->
<div id="app">
<h1 v-if="ok">Yes</h1>
<h1 v-else>No</h1>

</div>

<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
/*Model:数据*/
data:{
type: true
}
});
</script>
</body>
</html>

测试:

  1. 在浏览器上运行 打开控制台

  2. 在控制台输入 vm.ok = false 回车你会发现浏览器中显示的内容会直接变成NO

    注:v-*属性绑定数据是不需要双花括号包裹的

v-else-if
v-if
v-else-if
v-else

注:===三个等号在JS中表示绝对等于(就是数据与类型都要相等)上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en" xmlns:v-bind="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--view层,模板-->
<div id="app">
<h1 v-if="type==='A'">A</h1>
<h1 v-else-if="type==='B'">B</h1>
<h1 v-else-if="type==='D'">D</h1>
<h1 v-else>C</h1>

</div>

<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
/*Model:数据*/
data:{
type: 'A'
}
});
</script>
</body>
</html>

v-for

  • v-for

格式说明

1
2
3
4
5
6
7
<div id="app">
<li v-for="(item,index) in items">
{{item.message}}---{{index}}
</li>

</div>
123456

注:items是数组,item是数组元素迭代的别名。我们之后学习的Thymeleaf模板引擎的语法和这个十分的相似!

上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--view层,模板-->
<div id="app">
<li v-for="(item,index) in items">
{{item.message}}---{{index}}
</li>

</div>

<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
/*Model:数据*/
data:{
items:[
{message:'狂神说Java'},
{message:'狂神说前端'},
{message:'狂神说运维'}
]
}
});
</script>
</body>
</html>

测试:在控制台输入vm.items.push({message:'狂神说运维'}),尝试追加一条数据,你会发现浏览器中显示的内容会增加一条狂神说运维.

v-on

v-on监听事件
emsp;事件有Vue的事件、和前端页面本身的一些事件!我们这里的click是vue的事件, 可以绑定到Vue中的methods中的方法事件!
上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">
<button v-on:click="sayHi">点我</button>
</div>

<script src="../js/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
message:'Hello World'
},
methods:{
sayHi:function(event){
//'this'在方法里面指向当前Vue实例
alert(this.message);
}
}
});
</script>
</body>
</html>

点击测试
Vue还有一些基本的使用方式, 大家有需要的可以再跟着官方文档看看, 因为这些基本的指令几乎我们都见过了,一通百通!掌握学习的方式!

5. 表单双绑 组件

5.1 双向数据绑定

Vue.js是一个MV VM框架, 即数据双向绑定, 即当数据发生变化的时候, 视图也就发生变化, 当视图发生变化的时候,数据也会跟着同步变化。这也算是Vue.js的精髓之处了。
值得注意的是,我们所说的数据双向绑定,一定是对于UI控件来说的非UI控件不会涉及到数据双向绑定。单向数据绑定是使用状态管理工具的前提。如果我们使用vue x那么数据流也是单项的,这时就会和双向数据绑定有冲突。

为什么要实现数据的双向绑定

在Vue.js中,如果使用vuex, 实际上数据还是单向的, 之所以说是数据双向绑定,这是用的UI控件来说, 对于我们处理表单, Vue.js的双向数据绑定用起来就特别舒服了。即两者并不互斥,在全局性数据流使用单项,方便跟踪;局部性数据流使用双向,简单易操作

5.2 在表单中使用双向数据绑定

你可以用v-model指令在表单、及元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇, 但v-model本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。

注意:==v-model会忽略所有表单元素的value、checked、selected特性的初始值而总是将Vue实例的数据作为数据来源==。你应该通过JavaScript在组件的data选项中声明初始值!

(1)单行文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
输入的文本:<input type="text" v-model="message" value="hello">{{message}}
</div>

<script src="../js/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
message:""
}
});
</script>
</body>
</html>

(2)多行文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
多行文本:<textarea v-model="pan"></textarea>&nbsp;&nbsp;多行文本是:{{pan}}
</div>

<script src="../js/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
message:"Hello hello!"
}
});
</script>
</body>
</html>

(3)单复选框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">
单复选框:
<input type="checkbox" id="checkbox" v-model="checked">
&nbsp;&nbsp;
<label for="checkbox">{{checked}}</label>
</div>

<script src="../js/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
checked:false
}
});
</script>
</body>
</html>

(4)多复选框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">
多复选框:
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
&nbsp;&nbsp;
<label for="jack">Jack</label>
<input type="checkbox" id="join" value="Join" v-model="checkedNames">
&nbsp;&nbsp;
<label for="join">Jack</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
&nbsp;&nbsp;
<label for="mike">Mike</label>
<span>选中的值:{{checkedNames}}</span>
</div>

<script src="../js/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
checkedNames:[]
}
});
</script>
</body>
</html>

(5)单选按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">
单选框按钮
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<span>选中的值:{{picked}}</span>
</div>

<script src="../js/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
picked:'Two'
}
});
</script>
</body>
</html>

(7)下拉框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 性别:
<input type="radio" name="sex" value="男" v-model="pan">男
<input type="radio" name="sex" value="女" v-model="pan">女
<p>选中了:{{pan}}</p>-->

下拉框:
<select v-model="pan">
<option value="" disabled>---请选择---</option>
<option>A</option>
<option>B</option>
<option>C</option>
<option>D</option>
</select>
<span>value:{{pan}}</span>



</div>

<script src="../js/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
pan:"A"
}
});
</script>
</body>
</html>

注意v-model表达式的初始值未能匹配任何选项,元系将被渲染为“未选中”状态。 在iOS中, 这会使用户无法选择第一个选项,因为这样的情况下,iOS不会触发change事件。因此,更推荐像上面这样提供一个值为空的禁用选项

6.3 什么是组件

组件是可复用的Vue实例, 说白了就是一组可以重复使用的模板, 跟JSTL的自定义标签、Thymeleal的th:fragment等框架有着异曲同工之妙,通常一个应用会以一棵嵌套的组件树的形式来组织:

image-20220912014424040

image-20220912014431422

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件

第一个Vue组件

注意:在实际开发中,我们并不会用以下方式开发组件,而是采用vue-cli创建,vue模板文件的方式开发,以下方法只是为了让大家理解什么是组件。
使用Vue.component()方法注册组件,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<pan></pan>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
//先注册组件
Vue.component("pan",{

template:'<li>Hello</li>'

});
//再实例化Vue
var vm = new Vue({
el:"#app",
});
</script>

说明:

Vue.component():注册组件
pan:自定义组件的名字
template:组件的模板

使用props属性传递参数

像上面那样用组件没有任何意义,所以我们是需要传递参数到组件的,此时就需要使用props属性了!
注意:默认规则下props属性里的值不能为大写;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">
<!--组件:传递给组件中的值:props-->
<pan v-for="item in items" v-bind:panh="item"></pan>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
//定义组件
Vue.component("pan",{
props:['panh'],
template:'<li>{{panh}}</li>'

});
var vm = new Vue({
el:"#app",
data:{
items:["java","Linux","前端"]
}
});
</script>
</body>
</html>

说明:

  • v-for=”item in items”:遍历Vue实例中定义的名为items的数组,并创建同等数量的组件
  • v-bind:panh=”item”:将遍历的item项绑定到组件中props定义名为item属性上;= 号左边的panh为props定义的属性名,右边的为item in items 中遍历的item项的值

6. Axios异步通信

6.1 Axios概念

Axios是一个开源的可以用在浏览器端和Node JS的异步通信框架, 她的主要作用就是实现AJAX异步通信,其功能特点如下:

从浏览器中创建XMLHttpRequests
从node.js创建http请求
支持Promise API[JS中链式编程]
拦截请求和响应
转换请求数据和响应数据
取消请求
自动转换JSON数据
客户端支持防御XSRF(跨站请求伪造)
GitHub:https://github.com/axios/axios
中文文档:http://www.axios-js.com/

6.2 为什么使用Axios

由于Vue.js是一个视图层框架并且作者(尤雨溪) 严格准守SoC(关注度分离原则)所以Vue.js并不包含AJAX的通信功能, 为了解决通信问题, 作者单独开发了一个名为vue-resource的插件, 不过在进入2.0版本以后停止了对该插件的维护并推荐了Axios框架。少用jQuery, 因为它操作Dom太频繁

6.3 第一个Axios应用程序

咱们开发的接口大部分都是采用JSON格式, 可以先在项目里模拟一段JSON数据, 数据内容如下:创建一个名为data.json的文件并填入上面的内容, 放在项目的根目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"name": "狂神说Java",
"url": "https://blog.kuangstudy.com",
"page": 1,
"isNonProfit": true,
"address": {
"street": "含光门",
"city": "陕西西安",
"country": "中国"
},
"links": [
{
"name": "bilibili",
"url": "https://space.bilibili.com/95256449"
},
{
"name": "狂神说Java",
"url": "https://blog.kuangstudy.com"
},
{
"name": "百度",
"url": "https://www.baidu.com/"
}
]
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en" xmlns:v-binf="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!--v-cloak 解决闪烁问题-->
<style>
[v-cloak]{
display: none;
}
</style>
</head>
<body>
<div id="vue">
<div>地名:{{info.name}}</div>
<div>地址:{{info.address.country}}--{{info.address.city}}--{{info.address.street}}</div>
<div>链接:<a v-binf:href="info.url" target="_blank">{{info.url}}</a> </div>
</div>

<!--引入js文件-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#vue",
//data:属性:vm
data(){
return{
info:{
name:null,
address:{
country:null,
city:null,
street:null
},
url:null
}
}
},
mounted(){//钩子函数
axios
.get('data.json')
.then(response=>(this.info=response.data));
}
});
</script>
</body>
</html>

说明:

  1. 在这里使用了v-bind将a:href的属性值与Vue实例中的数据进行绑定
  2. 使用axios框架的get方法请求AJAX并自动将数据封装进了Vue实例的数据对象中
  3. 我们在data中的数据结构必须和Ajax响应回来的数据格式匹配!

6.4 Vue的生命周期

官方文档:https://cn.vuejs.org/v2/guide/instance.html#生命周期图示

Vue实例有一个完整的生命周期,也就是从开始创建初女台化数据、编译模板、挂载DOM、渲染一更新一渲染、卸载等一系列过程,我们称这是Vue的生命周期。通俗说就是Vue实例从创建到销毁的过程,就是生命周期。

在Vue的整个生命周期中,它提供了一系列的事件,可以让我们在事件触发时注册JS方法,可以让我们用自己注册的JS方法控制整个大局,在这些事件响应方法中的this直接指向的是Vue的实例。

vue生命周期

7. 计算属性 内容分发 自定义事件

什么是计算属性

计算属性的重点突出在属性两个字上(属性是名词),首先它是个属性其次这个属性有计算的能力(计算是动词),这里的计算就是个函数:简单点说,它就是一个能够将计算结果缓存起来的属性(将行为转化成了静态的属性),仅此而已;可以想象为缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--view层,模板-->
<div id="app">
<p>currentTime1:{{currentTime1()}}</p>
<p>currentTime2:{{currentTime2}}</p>
</div>

<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
message:"pan"
},
methods:{
currentTime1:function(){
return Date.now();//返回一个时间戳
}
},
computed:{
currentTime2:function(){//计算属性:methods,computed方法名不能重名,重名之后,只会调用methods的方法
this.message;
return Date.now();//返回一个时间戳
}
}
});
</script>
</body>
</html>

注意:methods和computed里的东西不能重名
说明:

  • methods:定义方法, 调用方法使用currentTime1(), 需要带括号
  • computed:定义计算属性, 调用属性使用currentTime2, 不需要带括号:this.message是为了能够让currentTime2观察到数据变化而变化
  • 如何在方法中的值发生了变化,则缓存就会刷新!可以在控制台使用vm.message=”q in jiang”, 改变下数据的值,再次测试观察效果!
  • 结论:
  •   调用方法时,每次都需要讲行计算,既然有计算过程则必定产生系统开销,那如果这个结果是不经常变化的呢?此时就可以考虑将这个结果缓存起来,采用计算属性可以很方便的做到这点,计算属性的主要特性就是为了将不经常变化的计算结果进行缓存,以节约我们的系统开销;

内容分发

在Vue.js中我们使用元素作为承载分发内容的出口,作者称其为插槽,可以应用在组合组件的场景中;

测试
比如准备制作一个待办事项组件(todo) , 该组件由待办标题(todo-title) 和待办内容(todo-items)组成,但这三个组件又是相互独立的,该如何操作呢?

  1. 第一步定义一个待办事项的组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<todo></todo>
</div>
<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
Vue.component('todo',{
template:'<div>\
<div>代办事项</div>\
<ul>\
<li>学习狂神说Java</li>\
</ul>\
</div>'
})
</script>
  1. 我们需要让,代办事项的标题和值实现动态绑定,怎么做呢?我们可以留一个插槽
  • 将上面的代码留出一个插槽,即slot
1
2
3
4
5
6
7
8
Vue.component('todo',{
template:'<div>\
<slot name="todo-title"></slot>\
<ul>\
<slot name="todo-items"></slot>\
</ul>\
</div>'
});
  • 定义一个名为todo-title的待办标题组件 和 todo-items的待办内容组件
1
2
3
4
5
6
7
8
9
10
11
Vue.component('todo-title',{
props:['title'],
template:'<div>{{title}}</div>'
});

12345
//这里的index,就是数组的下标,使用for循环遍历的时候,可以循环出来!
Vue.component("todo-items",{
props:["item","index"],
template:"<li>{{index+1}},{{item}}</li>"
});
  • 实例化Vue并初始化数据
1
2
3
4
5
6
var vm = new Vue({
el:"#vue",
data:{
todoItems:['test1','test2','test3']
}
});
  • 将这些值,通过插槽插入
1
2
3
4
5
6
7
8
<div id="vue">
<todo>
<todo-title slot="todo-title" title="秦老师系列课程"></todo-title>
<!--<todo-items slot="todo-items" v-for="{item,index} in todoItems" v-bind:item="item"></todo-items>-->
<!--如下为简写-->
<todo-items slot="todo-items" v-for="item in todoItems" :item="item"></todo-items
</todo>
</div>

说明:我们的todo-title和todo-items组件分别被分发到了todo组件的todo-title和todo-items插槽中

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--view层,模板-->
<div id="vue">
<todo>
<todo-title slot="todo-title" title="title"></todo-title>
<!--<todo-items slot="todo-items" v-for="{item,index} in todoItems" v-bind:item="item"></todo-items>-->
<!--如下为简写-->
<todo-items slot="todo-items" v-for="item in todoItems" :item="item"></todo-items
</todo>
</div>
<!--1.导入Vue.js-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>
<script type="text/javascript">
Vue.component('todo',{
template:'<div>\
<slot name="todo-title"></slot>\
<ul>\
<slot name="todo-items"></slot>\
</ul>\
</div>'
});
Vue.component('todo-title',{
props:['title'],
template:'<div>{{title}}</div>'
});
//这里的index,就是数组的下标,使用for循环遍历的时候,可以循环出来!
Vue.component("todo-items",{
props:["item","index"],
template:"<li>{{index+1}},{{item}}</li>"
});

var vm = new Vue({
el:"#vue",
data:{
title:"秦老师系列课程",
todoItems:['test1','test2','test3']
}
});
</script>
</body>
</html>

自定义事件

通以上代码不难发现,数据项在Vue的实例中, 但删除操作要在组件中完成, 那么组件如何才能删除Vue实例中的数据呢?此时就涉及到参数传递与事件分发了, Vue为我们提供了自定义事件的功能很好的帮助我们解决了这个问题; 使用this.$emit(‘自定义事件名’, 参数) , 操作过程如下:

  1. 在vue的实例中增加了methods对象并定义了一个名为removeTodoltems的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var vm = new Vue({
el:"#vue",
data:{
title_text:"秦老师系列课程",
todoItems:['test1','test2','test3']
},
methods:{
removeItems:function(index){
console.log("删除了"+this.todoItems[index]+"OK");
//splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目,其中index
this.todoItems.splice(index,1);
}
}
});
  1. 修改todo-items待办内容组件的代码,增加一个删除按钮,并且绑定事件
1
2
3
4
5
6
7
8
9
10
11
Vue.component("todo-items",{
props:["item_p","index_p"],
template:"<li>{{index_p+1}},{{item_p}} <button @click='remove'>删除</button></li>",
methods:{
remove:function (index) {
//这里的remove是自定义事件名称,需要在HTML中使用v-on:remove的方式
//this.$emit 自定义事件分发
this.$emit('remove',index);
}
}
});
  1. 修改todo-items待办内容组件的HTML代码,增加一个自定义事件,比如叫remove,可以和组件的方法绑定,然后绑定到vue的方法
1
2
3
<!--增加了v-on:remove="removeTodoItems(index)"自定义事件,该组件会调用Vue实例中定义的-->
<todo-items slot="todo-items" v-for="(item,index) in todoItems"
:item_p="item" :index_p="index" v-on:remove="removeItems(index)" :key="index"></todo-items>

对上一个代码进行修改,实现删除功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--view层,模板-->
<div id="vue">
<todo>
<todo-title slot="todo-title" :title="title_text"></todo-title>
<!--<todo-items slot="todo-items" v-for="(item,index) in todoItems" v-bind:item="item"></todo-items>-->
<!--如下为简写-->
<todo-items slot="todo-items" v-for="(item,index) in todoItems"
:item_p="item" :index_p="index" v-on:remove="removeItems(index)" :key="index"></todo-items>
</todo>
</div>
<!--1.导入Vue.js-->
<script src="../js/vue.js"></script>
<script type="text/javascript">
Vue.component('todo',{
template:'<div>\
<slot name="todo-title"></slot>\
<ul>\
<slot name="todo-items"></slot>\
</ul>\
</div>'
});
Vue.component('todo-title',{
props:['title'],
template:'<div>{{title}}</div>'
});
//这里的index,就是数组的下标,使用for循环遍历的时候,可以循环出来!
Vue.component("todo-items",{
props:["item_p","index_p"],
template:"<li>{{index_p+1}},{{item_p}} <button @click='remove_methods'>删除</button></li>",
methods:{
remove_methods:function (index) {
//this.$emit 自定义事件分发
this.$emit('remove',index);
}
}
});

var vm = new Vue({
el:"#vue",
data:{
title_text:"秦老师系列课程",
todoItems:['test1','test2','test3']
},
methods:{
removeItems:function(index){
console.log("删除了"+this.todoItems[index]+"OK");
this.todoItems.splice(index,1);
}
}
});
</script>
</body>
</html>

image-20220912101924545

Vue入门小结

核心:数据驱动,组件化

优点:借鉴了AngularJS的模块化开发和React的虚拟Dom,虚拟Dom就是把Demo操作放到内存中执行;

常用的属性:

  • v-if
  • v-else-if
  • v-else
  • v-for
  • v-on绑定事件,简写@
  • v-model数据双向绑定
  • v-bind给组件绑定参数,简写:

组件化:

  • 组合组件slot插槽
  • 组件内部绑定事件需要使用到this.$emit(“事件名”,参数);
  • 计算属性的特色,缓存计算数据

遵循SoC关注度分离原则,Vue是纯粹的视图框架,并不包含,比如Ajax之类的通信功能,为了解决通信问题,我们需要使用Axios框架做异步通信;

说明
Vue的开发都是要基于NodeJS,实际开发采用Vue-cli脚手架开发,vue-router路由,vuex做状态管理;Vue UI,界面我们一般使用ElementUI(饿了么出品),或者ICE(阿里巴巴出品)来快速搭建前端项目~~

官网:

8. 第一个vue-cli项目

什么是vue-cli

vue-cli官方提供的一个脚手架,用于快速生成一个vue的项目模板;预先定义好的目录结构及基础代码,就好比咱们在创建Maven项目时可以选择创建一个骨架项目,这个估计项目就是脚手架,我们的开发更加的快速;

vue-cli管理项目更偏向于 Linux系统

项目的功能:

  • 统一的目录结构
  • 本地调试
  • 热部署
  • 单元测试
  • 集成打包上线

所需环境

Node.js:http://nodejs.cn/download/
安装就是无脑的下一步就好,安装在自己的环境目录下
Git:https://git-scm.com/doenloads
镜像:https://npm.taobao.org/mirrors/git-for-windows/
确认nodejs安装成功:

cmd下输入node -v,查看是否能够正确打印出版本号即可!
cmd下输入npm -v,查看是否能够正确打印出版本号即可!
这个npm,就是一个软件包管理工具,就和linux下的apt软件安装差不多!
安装Node.js淘宝镜像加速器(cnpm)
这样的话,下载会快很多~

1
2
3
4
5
# -g 就是全局安装
npm install cnpm -g

# 或使用如下语句解决npm速度慢的问题
npm install --registry=https://registry.npm.taobao.org

安装的过程可能有点慢~,耐心等待!虽然安装了cnpm,但是尽量少用

安装的位置:C:\Users\administrator\AppData\Roaming\npm

image-20220912103054739

安装vue-cli

1
2
3
4
5
cnpm instal1 vue-cli-g

#测试是否安装成功
#查看可以基于哪些模板创建vue应用程序,通常我们选择webpack
vue list

image-20220912103121368

第一个vue-cli应用程序

  1. 创建一个Vue项目,我们随便建立一个空的文件夹在电脑上,我这里在D盘下新建一个目录
1
D:\Project\vue-study;
  1. 创建一个基于webpack模板的vue应用程序
1
2
3
#1、首先需要进入到对应的目录 cd D:\Project\vue-study
#2、这里的myvue是顶日名称,可以根据自己的需求起名
vue init webpack myvue

一路都选择no即可

说明:

  • Project name:项目名称,默认回车即可
  • Project description:项目描述,默认回车即可
  • Author:项目作者,默认回车即可
  • Install vue-router:是否安装vue-router,选择n不安装(后期需要再手动添加)
  • Use ESLint to lint your code:是否使用ESLint做代码检查,选择n不安装(后期需要再手动添加)
  • Set up unit tests:单元测试相关,选择n不安装(后期需要再手动添加)
  • Setupe2etests with Nightwatch:单元测试相关,选择n不安装(后期需要再手动添加)
  • Should we run npm install for you after the,project has been created:创建完成后直接初始化,选择n,我们手动执行;

运行结果

  • (1)初始化并运行
1
2
3
cd myvue
npm install
npm run dev

执行完成后,目录多了很多依赖

当出现问题时,可以查看提示进行处理如下

image-20220912110135082

image-20221015230547646

总体框架:一个vue-cli的项目结构如下,其中src文件夹是需要掌握的,所以本文也重点讲解其中的文件,至于其他相关文件,了解一下即可。

image-20221015230615043

vue-cli脚手架 文件结构细分

build——[webpack配置]

build文件主要是webpack的配置,主要启动文件是dev-server.js,当我们输入npm run dev首先启动的就是dev-server.js,它会去检查node及npm版本,加载配置文件,启动服务。

image-20221015230912318

config——[vue项目配置]

config文件主要是项目相关配置,我们常用的就是当端口冲突时配置监听端口,打包输出路径及命名等

image-20221015230939129

node_modules——[依赖包]

node_modules里面是项目依赖包,其中包括很多基础依赖,自己也可以根据需要安装其他依赖。

安装方法为打开cmd,进入项目目录,输入npm install [依赖包名称],回车。

在两种情况下我们会自己去安装依赖:

  1. 目运行缺少该依赖包:例如项目加载外部css会用到的css-loader,路由跳转vue-loader等(安装方法示例:npm install css-loader)
  2. 安装插件:如vux(基于WEUI的移动端组件库),vue-swiper(轮播插件)

注:有时会安装指定依赖版本,需在依赖包名称后加上版本号信息,如安装11.1.4版本的vue-loader,输入npm install vue-loader@11.1.4

src——[项目核心文件]

项目核心文件前面已经进行了简单的说明,接下来重点讲解main.js,App.vue及router的index.js

vue-cli脚手架 代码详解

image-20221015231235831

index.html——[主页]

一个简单的html页面,这里id=’app’,是为后面的设置vue作用域有关的

index.html如其他html一样,但一般只定义一个空的根节点,在main.js里面定义的实例将挂载在根节点下,内容都通过vue组件来填充

image-20221015231319623

Hello.vue——[Helle组件]

说明:在*.vue文件,template标签里写html代码,且template直接子级只能有一个标签。style标签里写样式,script里面写js代码

App.vue——[根组件]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/> //这里是用来展示路由页面内容的,如果想用跳转就用<router-link to='xxx'></router-link>
</div>
</template>

<script>
export default {
name: 'App'
}
</script>

<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

说明:一个vue页面通常由三部分组成:模板(template)、js(script)、样式(style)

  1. template
    其中模板只能包含一个父节点,也就是说顶层的div只能有一个(例如下图,父节点为#app的div,其没有兄弟节点)
    是子路由视图,后面的路由页面都显示在此处。打一个比喻吧,类似于一个插槽,跳转某个路由时,该路由下的页面就插在这个插槽中渲染显示
    image-20221016004128820

  2. script
    vue通常用es6来写,用export default导出,其下面可以包含数据data,生命周期(mounted等),方法(methods)等,具体语法请看vue.js文档,在后面我也会通过例子来说明

  3. style
    样式通过style标签包裹,默认是影响全局的,如需定义作用域只在该组件下起作用,需在标签上加scoped,
    如要引入外部css文件,首先需给项目安装css-loader依赖包,打开cmd,进入项目目录,输入npm install css-loader,回车。安装完成后,就可以在style标签下import所需的css文件

    1
    2
    3
    <style>
    import './assets/css/public.css'
    </style>

    这样,我们就可以把style下的样式封装起来,写到css文件夹,再引入到页面使用,整个vue页面也看上去更简洁。

main.js——[入口文件]

这个js文件是主页面配置的主入口。主要是利用ES6的模块化引入模板
main.js主要是引入vue框架,根组件及路由设置,并且定义vue实例,下面代码中的components:{App}就是引入的根组件App.vue
后期还可以引入插件,当然首先得安装插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue' // 引入vue文件
import App from './App'// 引入同目录下的App.vue模块
import router from './router'// 引入vue的路由

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
el: '#app',//定义作用范围就是index.html里的id为app的范围内
router,//引入路由
components: { App },//注册引入的组件App.vue
template: '<App/>'//给Vue实例初始一个App组件作为template 相当于默认组件
})

router——[路由配置]

router文件夹下,有一个index.js,即为路由配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'  //引用vue文件
import Router from 'vue-router' //引用vue路由模块,并赋值给变量Router
import HelloWorld from '@/components/HelloWorld' //英文HelloWorld.vue模版,并赋值给变量HelloWorld,这里是“@”相当于“../”

Vue.use(Router) //使用路由

export default new Router({
routes: [ //进行路由配置,规定“/”引入到HelloWorld组件
{
path: '/',
name: 'HelloWorld', //这个name暂时不知道用啥用,根据官方文档说的是方便排错的
component: HelloWorld //注册HelloWorld组件
}
]
})

这里定义了路径为’/‘的路由,该路由对应的页面是HelloWorld组件,所以当我们在浏览器url访问http://localhost:8080/#/时就渲染的Hello组件

类似的,我们可以设置多个路由,‘/index’,’/list’之类的,当然首先得引入该组件,再为该组件设置路由

说明:如果需要增加组件那就在components文件下定义xx.vue文件并编写代码即可;如果需要配置路由就要在index.js进行路由“路径”配置;还需要点击跳转就要用到标签了

参考文档:https://blog.csdn.net/dxnn520/article/details/123712506

9. webpack使用

9.1 什么是Webpack

本质上, webpack是一个现代JavaScript应用程序的静态模块打包器(module bundler) 。当webpack处理应用程序时, 它会递归地构建一个依赖关系图(dependency graph) , 其中包含应用程序需要的每个模块, 然后将所有这些模块打包成一个或多个bundle.

Webpack是当下最热门的前端资源模块化管理和打包工具, 它可以将许多松散耦合的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分离,等到实际需要时再异步加载。通过loader转换, 任何形式的资源都可以当做模块, 比如Commons JS、AMD、ES 6、CSS、JSON、Coffee Script、LESS等;

伴随着移动互联网的大潮, 当今越来越多的网站已经从网页模式进化到了WebApp模式。它们运行在现代浏览器里, 使用HTML 5、CSS 3、ES 6等新的技术来开发丰富的功能, 网页已经不仅仅是完成浏览器的基本需求; WebApp通常是一个SPA(单页面应用) , 每一个视图通过异步的方式加载,这导致页面初始化和使用过程中会加载越来越多的JS代码,这给前端的开发流程和资源组织带来了巨大挑战。

前端开发和其他开发工作的主要区别,首先是前端基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器的,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统,这个理想中的模块化系统是前端工程师多年来一直探索的难题

9.2 模块化的演进

Script标签

1
2
3
<script src = "module1.js"></script>
<script src = "module2.js"></script>
<script src = "module3.js"></script>

这是最原始的JavaScript文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在window对象中,不同模块的调用都是一个作用域。

这种原始的加载方式暴露了一些显而易见的弊端:

  • 全局作用域下容易造成变量冲突
  • 文件只能按照 < script >的书写顺序进行加载
  • 开发人员必须主观解决模块和代码库的依赖关系
  • 在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪

CommonJS

服务器端的NodeJS遵循CommonsJS规范,该规范核心思想是允许模块通过require方法来同步加载所需依赖的其它模块,然后通过exports或module.exports来导出需要暴露的接口

1
2
3
4
5
require("module");
require("../module.js");
export.doStuff = function(){};
module.exports = someValue;
1234

优点:

  • 服务器端模块便于重用
  • NPM中已经有超过45万个可以使用的模块包
  • 简单易用

缺点:

  • 同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的
  • 不能非阻塞的并行加载多个模块

实现:

  • 服务端的NodeJS
  • Browserify,浏览器端的CommonsJS实现,可以使用NPM的模块,但是编译打包后的文件体积较大
  • modules-webmake,类似Browserify,但不如Browserify灵活
  • wreq,Browserify的前身

AMD

Asynchronous Module Definition规范其实主要一个主要接口define(id?,dependencies?,factory);它要在声明模块的时候指定所有的依赖dependencies,并且还要当做形参传到factory中,对于依赖的模块提前执行。

1
2
3
4
define("module",["dep1","dep2"],functian(d1,d2){
return someExportedValue;
});
require(["module","../file.js"],function(module,file){});

优点

  • 适合在浏览器环境中异步加载模块
  • 可以并行加载多个模块

缺点

  • 提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不畅
  • 不符合通用的模块化思维方式,是一种妥协的实现

实现

  • RequireJS
  • curl

CMD

Commons Module Definition规范和AMD很相似,尽保持简单,并与CommonsJS和NodeJS的Modules规范保持了很大的兼容性。

1
2
3
4
5
6
define(function(require,exports,module){
var $=require("jquery");
var Spinning = require("./spinning");
exports.doSomething = ...;
module.exports=...;
});

优点:

  • 依赖就近,延迟执行
  • 可以很容易在NodeJS中运行缺点
  • 依赖SPM打包,模块的加载逻辑偏重

实现

  • Sea.js
  • coolie

ES6模块

EcmaScript 6标准增加了JavaScript语言层面的模块体系定义。ES 6模块的设计思想, 是尽量静态化, 使编译时就能确定模块的依赖关系, 以及输入和输出的变量。Commons JS和AMD模块,都只能在运行时确定这些东西。

1
2
3
import "jquery"
export function doStuff(){}
module "localModule"{}

优点

  • 容易进行静态分析
  • 面向未来的Ecma Script标准

缺点

  • 原生浏览器端还没有实现该标准
  • 全新的命令,新版的Node JS才支持

实现

  • Babel

大家期望的模块
系统可以兼容多种模块风格, 尽量可以利用已有的代码, 不仅仅只是JavaScript模块化, 还有CSS、图片、字体等资源也需要模块化。

9.3 安装Webpack

WebPack是一款模块加载器兼打包工具, 它能把各种资源, 如JS、JSX、ES 6、SASS、LESS、图片等都作为模块来处理和使用

1
2
npm install webpack -g
npm install webpack-cli -g

测试安装成功

  • webpack -v
  • webpack-cli -v

image-20220912165119921

配置

  1. 创建 webpack.config.js配置文件
    • entry:入口文件, 指定Web Pack用哪个文件作为项目的入口
    • output:输出, 指定WebPack把处理完成的文件放置到指定路径
    • module:模块, 用于处理各种类型的文件
    • plugins:插件, 如:热更新、代码重用等
    • resolve:设置路径指向
    • watch:监听, 用于设置文件改动后直接打包
  2. 直接运行webpack命令打包

9.4 使用webpack

  1. 创建项目
  2. 创建一个名为modules的目录,用于放置JS模块等资源文件
  3. 在modules下创建模块文件,如hello.js,用于编写JS模块相关代码
1
2
3
4
//暴露一个方法:sayHi
exports.sayHi = function(){
document.write("<div>Hello Webpack</div>");
}
  1. 在modules下创建一个名为main.js的入口文件,用于打包时设置entry属性
1
2
3
//require 导入一个模块,就可以调用这个模块中的方法了
var hello = require("./hello");
hello.sayHi();
  1. 在项目目录下创建webpack.config.js配置文件,使用webpack命令打包
1
2
3
4
5
6
7
module.exports = {
entry:"./modules/main.js",
output:{
filename:"./js/bundle.js"
}

}
  1. 在项目目录下创建HTML页面,如index.html,导入webpack打包后的JS文件
1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>狂神说Java</title>
</head>
<body>
<script src="dist/js/bundle.js"></script>
</body>
</html>
  1. 在IDEA控制台中直接执行webpack;如果失败的话,就使用管理员权限运行即可
  2. 运行HTML看效果
1
2
# 参数--watch 用于监听变化 --实现热部署
webpack --watch

10. vue-router路由

学习的时候,尽量的打开官方的文档

Vue Router是Vue.js官方的路由管理器。它和Vue.js的核心深度集成, 让构建单页面应用变得易如反掌。包含的功能有:

嵌套的路由/视图表
模块化的、基于组件的路由配置
路由参数、查询、通配符
基于Vue js过渡系统的视图过渡效果
细粒度的导航控制
带有自动激活的CSS class的链接
HTML5 历史模式或hash模式, 在IE 9中自动降级
自定义的滚动行为

安装

基于第一个vue-cli进行测试学习; 先查看node modules中是否存在vue-router

vue-router是一个插件包, 所以我们还是需要用n pm/cn pm来进行安装的。打开命令行工具,进入你的项目目录,输入下面命令。

1
npm install vue-router --save-dev

如果在一个模块化工程中使用它,必须要通过Vue.use()明确地安装路由功能:

1
2
3
4
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter);

测试

  1. 先删除没有用的东西
  2. components 目录下存放我们自己编写的组件
  3. 定义一个Content.vue 的组件
1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<h1>内容页</h1>
</div>
</template>

<script>
export default {
name:"Content"
}
</script>

Main.vue组件

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<h1>首页</h1>
</div>
</template>

<script>
export default {
name:"Main"
}
</script>
  1. 安装路由,在src目录下,新建一个文件夹:router,专门存放路由,配置路由index.js,如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Vue from'vue'
//导入路由插件
import Router from 'vue-router'
//导入上面定义的组件
import Content from '../components/Content'
import Main from '../components/Main'
//安装路由
Vue.use(Router) ;
//配置路由
export default new Router({
routes:[
{
//路由路径
path:'/content',
//路由名称
name:'content',
//跳转到组件
component:Content
},{
//路由路径
path:'/main',
//路由名称
name:'main',
//跳转到组件
component:Main
}
]
});
  1. main.js中配置路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Vue from 'vue'
import App from './App'

//导入上面创建的路由配置目录
import router from './router'//自动扫描里面的路由配置

//来关闭生产模式下给出的提示
Vue.config.productionTip = false;

new Vue({
el:"#app",
//配置路由
router,
components:{App},
template:'<App/>'
});
  1. App.vue中使用路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div id="app">
<!--
router-link:默认会被渲染成一个<a>标签,to属性为指定链接
router-view:用于渲染路由匹配到的组件
-->
<router-link to="/">首页</router-link>
<router-link to="/content">内容</router-link>
<router-view></router-view>
</div>
</template>

<script>
export default{
name:'App'
}
</script>
<style></style>

11. 实战快速上手

我们采用实战教学模式并结合ElementUI组件库,将所需知识点应用到实际中,以最快速度带领大家掌握Vue的使用;

创建工程

注意:命令行都要使用管理员模式运行

  1. 创建一个名为hello-vue的工程vue init webpack hello-vue
  2. 安装依赖, 我们需要安装vue-router、element-ui、sass-loader和node-sass四个插件
1
2
3
4
5
6
7
8
9
10
11
12
#进入工程目录
cd hello-vue
#安装vue-routern
npm install vue-router --save-dev
#安装element-ui
npm i element-ui -S
#安装依赖
npm install
# 安装SASS加载器
cnpm install sass-loader node-sass --save-dev
#启功测试
npm run dev
  1. Npm命令解释:

    npm install moduleName:安装模块到项目目录下

    npm install -g moduleName:-g的意思是将模块安装到全局,具体安装到磁盘哪个位置要看npm config prefix的位置

    npm install -save moduleName:–save的意思是将模块安装到项目目录下, 并在package文件的dependencies节点写入依赖,-S为该命令的缩写

    npm install -save-dev moduleName:–save-dev的意思是将模块安装到项目目录下,并在package文件的devDependencies节点写入依赖,-D为该命令的缩写

创建登录页面

把没有用的初始化东西删掉!

在源码目录中创建如下结构:

  • assets:用于存放资源文件
  • components:用于存放Vue功能组件
  • views:用于存放Vue视图组件
  • router:用于存放vue-router配置

image-20220912173011400

创建首页视图,在views目录下创建一个名为Main.vue的视图组件:

1
2
3
4
5
6
7
8
9
10
<template>
<div>首页</div>
</template>
<script>
export default {
name:"Main"
}
</script>
<style scoped>
</style>

创建登录页视图在views目录下创建名为Login.vue的视图组件,其中el-*的元素为ElementUI组件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<template>
<div>
<el-form ref="loginForm" :model="form" :rules="rules" label-width="80px" class="login-box">
<h3 class="login-title">欢迎登录</h3>
<el-form-item label="账号" prop="username">
<el-input type="text" placeholder="请输入账号" v-model="form.username"/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" placeholder="请输入密码" v-model="form.password"/>
</el-form-item>
<el-form-item>
<el-button type="primary" v-on:click="onsubmit('loginForm')">登录</el-button>
</el-form-item>
</el-form>

<el-dialog title="温馨提示" :visible.sync="dialogVisiable" width="30%" :before-close="handleClose">
<span>请输入账号和密码</span>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogVisible = false">确定</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
export default {
name: "Login",
data(){
return{
form:{
username:'',
password:''
},
//表单验证,需要在 el-form-item 元素中增加prop属性
rules:{
username:[
{required:true,message:"账号不可为空",trigger:"blur"}
],
password:[
{required:true,message:"密码不可为空",tigger:"blur"}
]
},

//对话框显示和隐藏
dialogVisible:false
}
},
methods:{
onSubmit(formName){
//为表单绑定验证功能
this.$refs[formName].validate((valid)=>{
if(valid){
//使用vue-router路由到指定界面,该方式称为编程式导航
this.$router.push('/main');
}else{
this.dialogVisible=true;
return false;
}
});
}
}
}
</script>

<style lang="scss" scoped>
.login-box{
border:1px solid #DCDFE6;
width: 350px;
margin:180px auto;
padding: 35px 35px 15px 35px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909399;
}
.login-title{
text-align:center;
margin: 0 auto 40px auto;
color: #303133;
}
</style>

==版本升级后 Login.vue这段代码有问题==

创建路由,在router目录下创建一个名为index.js的vue-router路由配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//导入vue
import Vue from 'vue';
import VueRouter from 'vue-router';
//导入组件
import Main from "../views/Main";
import Login from "../views/Login";
//使用
Vue.use(VueRouter);
//导出
export default new VueRouter({
routes: [
{
//登录页
path: '/main',
component: Main
},
//首页
{
path: '/login',
component: Login
},
]

})

APP.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div id="app">
<router-view></router-view>
</div>
</template>

<script>


export default {
name: 'App',

}
</script>

<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from "./router"

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(router)
Vue.use(ElementUI)

/* eslint-disable no-new */
new Vue({
el: '#app',
router,
render:h=>h(App)
})

测试:在浏览器打开 http://localhost:8080/#/login

如果出现错误: 可能是因为sass-loader的版本过高导致的编译错误,当前最高版本是8.0.2,需要退回到7.3.1 ;

去package.json文件里面的 “sass-loader”的版本更换成7.3.1,然后重新cnpm install就可以了;

路由嵌套

嵌套路由又称子路由,在实际应用中,通常由多层嵌套的组件组合而成。

demo

  1. 创建用户信息组件,在 views/user 目录下创建一个名为 Profile.vue 的视图组件;

Profile.vue

1
2
3
4
5
6
7
8
9
10
<template>
<h1>个人信息</h1>
</template>
<script>
export default {
name: "UserProfile"
}
</script>
<style scoped>
</style>
  1. 在用户列表组件在 views/user 目录下创建一个名为 List.vue 的视图组件;

List.vue

1
2
3
4
5
6
7
8
9
10
<template>
<h1>用户列表</h1>
</template>
<script>
export default {
name: "UserList"
}
</script>
<style scoped>
</style>
  1. 修改首页视图,我们修改 Main.vue 视图组件,此处使用了 ElementUI 布局容器组件,代码如下:

Main.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<template>
<div>
<el-container>
<el-aside width="200px">
<el-menu :default-openeds="['1']">
<el-submenu index="1">
<template slot="title"><i class="el-icon-caret-right"></i>用户管理</template>
<el-menu-item-group>
<el-menu-item index="1-1">
<!--插入的地方-->
<router-link to="/user/profile">个人信息</router-link>
</el-menu-item>
<el-menu-item index="1-2">
<!--插入的地方-->
<router-link to="/user/list">用户列表</router-link>
</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-caret-right"></i>内容管理</template>
<el-menu-item-group>
<el-menu-item index="2-1">分类管理</el-menu-item>
<el-menu-item index="2-2">内容列表</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>

<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-header>
<el-main>
<!--在这里展示视图-->
<router-view />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default {
name: "Main"
}
</script>
<style scoped lang="scss">
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}
.el-aside {
color: #333;
}
</style>
  1. 配置嵌套路由修改 router 目录下的 index.js 路由配置文件,使用children放入main中写入子模块,代码如下

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//导入vue
import Vue from 'vue';
import VueRouter from 'vue-router';
//导入组件
import Main from "../views/Main";
import Login from "../views/Login";
//导入子模块
import UserList from "../views/user/List";
import UserProfile from "../views/user/Profile";

//使用
Vue.use(VueRouter);
//导出
export default new VueRouter({
routes: [
{
//登录页
path: '/main',
component: Main,
// 写入子模块
children: [
{
path: '/user/profile',
component: UserProfile,
}, {
path: '/user/list',
component: UserList,
},
]
},
//首页
{
path: '/login',
component: Login

},
]
})
  1. 路由嵌套实战效果图

image-20220912173423604

参数传递

这里演示如果请求带有参数该怎么传递

demo
用的还是上述例子的代码 修改一些代码 这里不放重复的代码了
第一种取值方式

  1. 修改路由配置, 主要是router下的index.js中的 path 属性中增加了 :id 这样的占位符
1
2
3
4
5
{
path: '/user/profile/:id',
name:'UserProfile',
component: UserProfile
}
  1. 传递参数

此时我们在Main.vue中的route-link位置处 to 改为了 :to,是为了将这一属性当成对象使用,注意 router-link 中的 name 属性名称 一定要和 路由中的 name 属性名称 匹配,因为这样 Vue 才能找到对应的路由路径;

1
2
<!--name是组件的名字 params是传的参数 如果要传参数的话就需要用v:bind:来绑定-->
<router-link :to="{name:'UserProfile',params:{id:1}}">个人信息</router-link>
  1. 在要展示的组件Profile.vue中接收参数 使用 来接收

Profile.vue 部分代码

image-20220912173621362

1
2
3
4
5
6
7
<template>
<!-- 所有的元素必须在根节点下-->
<div>
<h1>个人信息</h1>
{{$route.params.id}}
</div>
</template>

第二种取值方式 使用props 减少耦合

  1. 修改路由配置 , 主要在router下的index.js中的路由属性中增加了 props: true 属性
1
2
3
4
5
6
{
path: '/user/profile/:id',
name:'UserProfile',
component: UserProfile,
props: true
}
  1. 传递参数和之前一样 在Main.vue中修改route-link地址
1
2
<!--name是组件的名字 params是传的参数 如果要传参数的话就需要用v:bind:来绑定-->
<router-link :to="{name:'UserProfile',params:{id:1}}">个人信息</router-link>
  1. 在Profile.vue接收参数为目标组件增加 props 属性

Profile.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
个人信息
{{ id }}
</div>
</template>
<script>
export default {
props: ['id'],
name: "UserProfile"
}
</script>
<style scoped>
</style>

image-20220912173743111

组件重定向

重定向的意思大家都明白,但 Vue 中的重定向是作用在路径不同但组件相同的情况下,比如:

在router下面index.js的配置

1
2
3
4
5
6
7
8
9
{
path: '/main',
name: 'Main',
component: Main
},
{
path: '/goHome',
redirect: '/main'
}

说明:这里定义了两个路径,一个是 /main ,一个是 /goHome,其中 /goHome 重定向到了 /main 路径,由此可以看出重定向不需要定义组件;

使用的话,只需要在Main.vue设置对应路径即可;

1
2
3
<el-menu-item index="1-3">
<router-link to="/goHome">回到首页</router-link>
</el-menu-item>

路由模式与 404

路由模式有两种

修改路由配置,代码如下:

1
2
3
4
5
export default new Router({
mode: 'history',
routes: [
]
});

404 demo

  1. 创建一个NotFound.vue视图组件

NotFound.vue

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<h1>404,你的页面走丢了</h1>
</div>
</template>
<script>
export default {
name: "NotFound"
}
</script>
<style scoped>
</style>
  1. 修改路由配置index.js
1
2
3
4
5
import NotFound from '../views/NotFound'
{
path: '*',
component: NotFound
}
  1. 效果图

image-20220912173947313

图 404效果图

路由钩子与异步请求

beforeRouteEnter:在进入路由前执行
beforeRouteLeave:在离开路由前执行

在Profile.vue中写

1
2
3
4
5
6
7
8
9
10
11
export default {
name: "UserProfile",
beforeRouteEnter: (to, from, next) => {
console.log("准备进入个人信息页");
next();
},
beforeRouteLeave: (to, from, next) => {
console.log("准备离开个人信息页");
next();
}
}

参数说明:

to:路由将要跳转的路径信息
from:路径跳转前的路径信息
next:路由的控制参数
next() 跳入下一个页面
next(’/path’) 改变路由的跳转方向,使其跳到另一个路由
next(false) 返回原来的页面
next((vm)=>{}) 仅在 beforeRouteEnter 中可用,vm 是组件实例

在钩子函数中使用异步请求

  1. 安装 Axios
1
cnpm install --save vue-axios
  1. main.js引用 Axios
1
2
3
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
  1. 准备数据 : 只有我们的 static 目录下的文件是可以被访问到的,所以我们就把静态文件放入该目录下。

    数据和之前用的json数据一样 需要的去上述axios例子里

1
2
// 静态数据存放的位置
static/mock/data.json
  1. 在 beforeRouteEnter 中进行异步请求

Profile.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export default {
//第二种取值方式
// props:['id'],
name: "UserProfile",
//钩子函数 过滤器
beforeRouteEnter: (to, from, next) => {
//加载数据
console.log("进入路由之前")
next(vm => {
//进入路由之前执行getData方法
vm.getData()
});
},
beforeRouteLeave: (to, from, next) => {
console.log("离开路由之前")
next();
},
//axios
methods: {
getData: function () {
this.axios({
method: 'get',
url: 'http://localhost:8080/static/mock/data.json'
}).then(function (response) {
console.log(response)
})
}
}
}
  1. 路由钩子和axios结合图

image-20220912174204573

Tips

  1. 第一次使用Vue不要忘记初始化
  2. vscode安装插件 –Live Server –Vetur –view-in-browser

Nodejs

1. 预科准备

  1. 前端开发工具
  2. Nodejs安装
  3. ES6新语法糖
  4. npm包管理器
  5. Babel –新一代JavaScript编译器
  6. 模块化管理
  7. Webpack打包 编译
  8. 快速构建nodejs项目 –vue-admin-element实例应用
  9. 快速构建nodejs项目antd

前端工具vscode

  1. 下载安装vscode
  2. 傻瓜式安装
  3. 左侧工具栏 从上到下依次为 目录结构 代码搜索及替换 版本控制 运行及调试 插件市场
  4. emment 自动补全 语法学习

Nodejs安装与应用

Nodejs是一个JavaScript运行的服务端开发平台 使JavaScript成为与php python perl ruby 等服务端语言平起平坐的脚本语言 由Ryan Dahl开发 实质是将Chrome V8引擎进行封装

通俗讲Nodejs就是运行在服务端的JavaScript Nodejs是一个基于Chrom JavaScript 运行时建立的一个平台 底层架构是 JavaScript 文件后缀为.js

同时Nodejs 是一个 事件驱动 I/O 的服务端JavaScript环境 基于Google的V8引擎 V8引擎执行JavaScript的速度非常快 性能非常好

  1. .msi文件 傻瓜式安装

  2. node -v ``npm -v` 查看 安装后的版本 测试是否安装成功

  3. Nodejs参考文献

  4. .js文件 为JavaScript脚本 直接终端 node chapter01 即可运行

.java .js
代码运行流程 API jdk API Nodejs
JVM --> 将.class字节码文件解析给操作系统 Chrome V8引擎 --> 将.js文件解释给操作系统
操作系统 操作系统

2. Nodejs 实现请求响应

require

require 是导入模块 类似于 import java.io

1
const http = require('http');

请求响应

请求响应事件步骤:

  1. 创建一个httpserver服务
  2. 监听一个端口 8888
  3. 启动运行服务node httpserver.js
  4. 在浏览器中访问http://localhost:8888/测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* @Description:
* @Version: 1.0
* @Autor: negativeFunction
* @Date: 2022-09-24 00:27:29
* @FilePath: \Test\chapter02.js
* @LastEditors: negativeFunction
* @LastEditTime: 2022-09-24 08:35:26
*/
// 导入模块 require 类似于 import java.io
const http = require('http');

// 1. 创建一个HTTP server服务
http.createServer(function(request, response) {
// 浏览器如何识别 hello world
response.writeHead(200, {'Content-type':'text/html'}); // 含义 --告知浏览器将以 text/plain 去解析hello world 这段数据
// 给浏览器输出内容 若上句解析为 text/html 就会以html去解析 text/plain
response.end("<h1>hello world</h1>");
}).listen(8888);

console.log("已启动端口为8888 的http服务 --> chapter02 访问路径为: http://localhost:8888/");

// 2. 监听一个端口8888
// 3. 启动运行服务 node httpserver.js
// 使用node 运行js 时 可直接 node httpserver 省去js后缀
// 4. 在浏览器中访问http://localhost:8888/测试

3. Nodejs 操作MySQL数据库

参考文档:https://www.npmjs.com/package/mysql

操作步骤:

  1. 安装mysql 第三方依赖 npm install mysql
  2. 创建db.js进行操作数据库代码编写
    1. 导入mysql依赖包
    2. 创建一个mysql的Connection对象
    3. 配置数据连接信息
    4. 开辟连接
    5. 执行CRUD
    6. 关闭连接
  3. 运行 node db.js 测试
    使用node 运行js 时 可直接 node httpserver 省去js后缀

4. ES6概述

ES6概述

ECMAScript 6(简称ES6)是于2015年6月正式发布的JavaScript语言的标准,正式名为ECMAScript 2015(ES2015)。它的目标是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言

ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会,European Computer Manufacturers Association)通过ECMA-262标准化的脚本程序设计语言。这种语言在万维网上应用广泛,它往往被称为JavaScriptJScript,所以它可以理解为是JavaScript的一个标准,但实际上后两者是ECMA-262标准的实现和扩展。

ECMAScript是由网景的布兰登·艾奇开发的一种脚本语言的标准化规范;最初命名为Mocha,后来改名为LiveScript,最后重命名为JavaScript。1995年12月,升阳与网景联合发表了JavaScript。1996年11月,网景公司将JavaScript提交给欧洲计算机制造商协会进行标准化。ECMA-262的第一个版本于1997年6月被Ecma组织采纳。ECMA Script是ECMA-262标准化的脚本语言的名称。尽管JavaScript和JScript与ECMAScript兼容,但包含超出ECMA Script的功能。

ECMAScript是一种可以在宿主环境中执行计算并能操作可计算对象的基于对象的程序设计语言。ECMAScript最先被设计成一种Web脚本语言,用来支持Web页面的动态表现以及为基于Web的客户机—服务器架构提供服务器端的计算能力。但作为一种脚本语言, ECMAScript具备同其他脚本语言一样的性质,即“用来操纵、定制一个已存在系统所提供的功能,以及对其进行自动化”。

2022年6月22日,第 123 届 ECMA 大会批准了 ECMAScript 2022 语言规范,这意味着它现在正式成为标准。下面就来看看 ECMAScript 2022

ECMAScript版本发展

  • 1998.6 ES2.0发布
  • 1999.1 ES3.0发布
  • 2007.10 ES4.0起草
  • 2009.12 ES5.0发布
  • 2011.6 ES5.1发布
  • 2015.6 ES6.0发布

4.1 ES6语法:let 和 const命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>


<script>
// 传统定义变量和常量的方式 统一使用 var
var name = "nihaoya";
var link = "www.baidu.com";
var PI = Math.PI;
console.log(name);
console.log(link);
console.log(PI);

// ES6 定义方式 区分了常量和变量
let name2 = "nibuhaoya";
let link2 = "www.weibo.com";

// 定义常量
const PI2 = Math.PI;
console.log(name2);
console.log(link2);
console.log(PI2);

// let 与 const 解决
// 1. var 变量穿透问题
// 2. 常量修改问题
for (let i = 0; i < 5; i++) {
console.log(i);
}

// 再次console.log 就会造成变量穿透问题
console.log(i);
// 使用let替换就能解决 穿透 --> 会直接报错 i未定义

PI = 100;
PI2 = 100;
// 注意在实际开发和生产中 如果是小程序 uniapp 或者一些脚手架 可以大胆使用let 和 const
// 但是如果在web开发中 不建议使用 还是建议使用var 因为在低版本浏览器中 并不支持 let 和 const

</script>
</body>
</html>

4.2 ES6语法:模板字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>


<script>
var person = {
name: "lin",
address: "hello world",
link: "www.baidu.com"
}
let address = "传统js I am " + person.name + "I want to say " + person.address;
console.log(address);

let address2 = `ES6 I am ${person.name} I want to say ${person.address}`;
console.log(address2);
</script>
</body>
</html>

4.3 ES6语法:函数默认参数与箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>


<script>
// 默认函数
function f(a, b) {
return a + b;
}
var result = f(199, 233);
console.log(result);


// 箭头函数 --重点 在比如 小程序 uniapp 常见脚手架中 大量使用
const x = (a, b) => {
return a + b;
};
console.log(x(12, 44));
// 写法一
var y = (a, b) => {
return a + b;
};

// 写法二
var sum = (a, b) => a + b;

console.log(y(132, 44));
console.log(sum(1352, 44));

var arr = [1, 3, 5];
var answer = arr.map((a, b) => a + b);
console.log(answer);
// 如果 参数 只有一个 可以 括号也省去
// 多个参数 则不能省去

</script>
</body>
</html>

4.4 ES6语法:对象初始化简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<!--
* @Description:
* @Version: 1.0
* @Autor: negativeFunction
* @Date: 2022-07-28 17:53:32
* @FilePath: \Demo01\demo02.html
* @LastEditors: negativeFunction
* @LastEditTime: 2022-07-28 18:00:35
-->
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>

<form action="">
<p>账号: <input type="text" name="account" id="account"></p>
<p>密码: <input type="password" name="password" id="password"></p>
<p><input type="button" id="loginbtn" value="login"></p>
</form>

<script>
// 案例
// document.getElementById("loginbtn").onclick = function () {}

$("#loginbtn").on("click", function () {
var account = $("#account").val();
var password = $("#password").val();

// 对象简写 应用场景
var params = {account, password};

// 执行异步请求
$.ajax({
type: "post",
url: "xxx",
data: params,
success() {
console.log("abc");
}
})



});




// 传统对象
let info = {
title: "hello world",
link: "www.baidu.com",
Autor: "lin",
goto: function () {
console.log("hello");
}
}
console.log(info);
info.goto();
console.log(info.title);
console.log(info.link);
console.log(info.Autor);

/*
* ES6 简写
* js对象是以 key:value 键值对存在
* 1. 如果key和变量的名字一致 可以只定义一次
* 2. 如果value 是一个函数 可以讲 `:function` 去掉 只剩下 `()` 即可
* */

console.info("------------------------------------------");

var link2 = "www.4399.com";
var title2 = "4399";
let info2 = {
title2,
link2,
go() {
console.log("nishigedashagua");
}
};

console.log(info2);
console.log(info2.title2);
console.log(info2.link2);
info2.go();


</script>
</body>
</html>

4.5 ES6语法:对象解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!--
* @Description:
* @Version: 1.0
* @Autor: negativeFunction
* @Date: 2022-07-28 17:53:32
* @FilePath: \Demo01\demo02.html
* @LastEditors: negativeFunction
* @LastEditTime: 2022-07-28 18:00:35
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>


<script>
/*
* 对象是以 key: value 的形式存在
* 获取对象属性和方法的方式有两种
* 1. 通过 .
* 2. 通过 []
* */

var link2 = "www.4399.com";
var title2 = "4399";
let info2 = {
title2,
link2,
go() {
console.log("nishigedashagua");
}
};

console.log(info2);

// 通过 .
console.log(info2.title2);
console.log(info2.link2);
info2.go();

console.log("-------------------------------------------------------")

// 通过 []
console.log(info2["link2"]);
console.log(info2["title2"]);
info2["go"]();

console.log("-------------------------------------------------------")

// ES6 对象解构 -快速获取属性和方法的一种形式
var {title2, link2, go} = info2;
// 还原代码
// var title2 = info2.title2;
// var link2 = info2.link2;
console.log(title2);
console.log(link2);
go();

// 为什么 对象的取值 提供两种方式 . []

</script>
</body>
</html>

4.6 ES6语法:传播操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!--
* @Description:
* @Version: 1.0
* @Autor: negativeFunction
* @Date: 2022-07-28 17:53:32
* @FilePath: \Demo01\demo02.html
* @LastEditors: negativeFunction
* @LastEditTime: 2022-07-28 18:00:35
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>


<script>
// 对象传播操作符 ...
var person = {
name: "lin",
address: "China",
link: "https://www.baidu.com",
phone: "11111",
go() {
console.log("go to school");
}
};

// 解构
var {name, address, ...person2} =person;
console.log(name);
console.log(address);
console.log(person2);

// 应用案例
/*
* java --后台
* 数据格式 var userPage = {pages: 10, users: [{}, {}], pageNo: 1, pageSize: 100, total: 100};
* 异步请求
* $.post("/user/search", function(res) {}
* */

res = {pages: 10, users: [{}, {}], pageNo: 1, pageSize: 100, total: 100};
var userPage = {pages: 10, users: [{}, {}], pageNo: 1, pageSize: 100, total: 100};
var {users, total, pageNo, pages} = userPage;

</script>
</body>
</html>

4.7 ES6语法:数组 map 和 reduce 方法使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!--
* @Description:
* @Version: 1.0
* @Autor: negativeFunction
* @Date: 2022-07-28 17:53:32
* @FilePath: \Demo01\demo02.html
* @LastEditors: negativeFunction
* @LastEditTime: 2022-07-28 18:00:35
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>


<script>
// 对arr 数组每个元素 *2
let arr = [1, 2, 3, 4];

// 传统方式
let newArr = [];
for (let i = 0; i < arr.length; i++) {
newArr.push(arr[i] * 2);
};
console.log(newArr);

// map增强
// map中自带循环遍历 并且在循环时 会将处理的值 回填到对应位置
var newArr2 = arr.map(function (ele) {
return ele * 2
});
var newArr3 = arr.map( ele => ele * 2);

// 不能这样写
// var ele = [1, 2, 3, 4]; 无法找到函数参数ele定义
// var newArr4 = ele.map( () => ele * 2);

console.log("2---" + newArr2);
console.log("3---" + newArr3);

// map 处理对象数据
var users = [{age: 10, name: "xiaoming"}, {age: 12, name: "xiaohong"}, {age: 15, name: "xiaolan"}];
var newUsers = users.map(ele => {
ele.age += 1;
return ele;
});
console.log(newUsers);

// 将数组不断依次 取出两个 相加后赋值到原来的位置 直至参数不够 一次性出去两个
/*
* 1 2 3 4
* => 1 + 2 = 3
* 3 3 4
* => 3 + 3 = 6
* 6 4
* => 6 + 4 = 10
* log 10
* */
var result = arr.reduce(function (a, b) {
return a + b;
})
console.log("result==>" + result);

</script>
</body>
</html>

5. npm包管理器

npm包管理器简介

官网:https://www.npmjs.com/

npm全球Node Package Manager 是Nodejs包管理工具 号称全球最大的模块生态系统 里面所有的模块都是开源免费的 也是Nodejs的包管理工具 相当于前端的Maven 地位对比Maven在Java中的地位

简单操作:

  • npm -v 查看当前npm 版本 –在安装nodejs时 会自动安装npm

  • 用于 快速构建 nodejs 工程 和 快速安装依赖与第三方模块 如 npm install redis npm install mysql

  • 项目骨架构建步骤:

    1. 创建文件夹 并在文件夹中 执行 npm init 选择对应需要的参数 构建npm 项目 使用 npm inti -y 可以一键默认值创建项目初始化

    2. 得到package.json 即 在执行 npm init 时选择的参数与信息
      package.json 类似于 pom.xml 文件用于管理第三方依赖

    3. 快速安装第三方依赖 通过 npm install xxx 或者 npm i redis模块名 来安装第三方依赖

      执行命令后 会自动生成 node_modules 文件夹来存放第三方项目依赖

      安装完成后 require 导入依赖并使用 –参考官方文档

      通过 npm install xxx 安装的依赖会 记录在package.json 这个文件中 就类似于maven 将依赖以及项目信息记录在 pom.xml中类似

      记录的作用在于 –> 便于复用 所安装的依赖 同时 适配版本

      不直接拷贝node_modules文件夹的原因是 一个完整的前端项目 所包含的node_modules第三方依赖数量较为庞大 可能多大上万个文件 直接拷贝的速率会相当低 同时也存在版本更新问题

    4. 更改 npm 镜像 为淘宝镜像

      npm install -g cnpm --registry=https://registry.npm.taobao.org

    5. 一次性下载多个依赖npm install redis jquery@1.4.5 mysql
      具体版本号查看官网 https://www.npmjs.com/package/redis

    6. 卸载模块 通过npm uninstall veu redis

6. Babel

Babel 简介

ES6的部分高级语法 在浏览器甚至Nodejs环境中无法运行

Babel 是一个广泛的转码器 用于将ES6转码成ES5代码 从而能满足Nodejs和浏览器的环境适配 –即可以使用ES6代码规范 语法糖来编写程序 而不用担心环境是否支持

官网:https://www.babeljs.cn/

Babel 自带了一个内置的 CLI 命令行工具 –babe-cli,可通过命令行编译文件。

此外,各种可直接调用脚本都存放在 @babel/cli/bin 中。一个可通过 shell 执行的实用脚本 - babel-external-helpers.js,以及 Babel cli 主脚本 babel.js

安装以及简单操作步骤

命令行安装babel转码工具 –babel提供babel-cli工具 用于命令行 转码

具体安装步骤:

  1. npm install -g babel cli 安装babel-cli 工具 –实际操作不确定成功

    1
    2
    3
    // 若不成功 则使用以下命令
    cnpm install babel -g
    cnpm install babel-cli -g
  2. babel --version 查看是否安装成功 以及版本

简单应用操作:

  1. 创建babel工程文件夹

  2. 初始化项目 npm init -y

  3. 创建文件src/example.js 并编写一段ES6代码

    1
    2
    3
    4
    5
    6
    // 转码前
    // 定义数据
    let input = [1, 2, 3];
    // 将数组每个元素都 + 1
    input = input.map( params => params + 1);
    console.log(input)
  4. 配置 .babelrc
    babel的配置文件是 .babelrc 存放在项目的根目录下 该文件用来设置转码规则和插件 基本格式如下

    1
    2
    3
    4
    {
    "presets": [],
    "plugins": []
    }

    presets字段设定转码规则 将ES2015规则加入 .babelrc

    1
    2
    3
    4
    {
    "presets": ["es2015"],
    "plugins": []
    }
  5. 使用命令npm install --save-dev babel-preset-es2015安装转码器 在项目中安装

  6. 通过执行 babel src -d dist命令进行 转换

自定义脚本运行babel

具体操作步骤:

  1. 改写package.json

    1
    2
    3
    4
    5
    6
    7
    {
    //...
    "script": {
    //...
    "build": "babel src\\example.js -o dist\\compiled.js"
    },
    }
  2. 转码时 执行以下命令

    1
    2
    mkdir dist
    npm run build

7. 模块化

模块化简介

模块化产生背景

随着网站逐渐变成 “互联网应用程序” 嵌入网页的JavaScript代码也越来越庞大 越来越复杂

JavaScript模块化编程 成为一种迫切需求 在模块化开发下 开发者只需要实现核心业务逻辑 其他只需要加载第三方模块 但早年的JavaScript并不是一种模块化编程语言 它不支持 类class 包package 等概念 也不支持模块 module

故而JavaScript需要进阶成模块化编程

模块化规范

  • CommonJS模块化规范 –require mysql require app
  • ES6模块化规范 –vue uniapp

CommonJS模块化规范

需要导入的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 工具类
const sum = (a, b) => a + b;
const sub = (a, b) => a - b;
const mul = (a, b) => a * b;
const di = (a, b) => a / b;

// 导入工具类 提供给别人使用
// module.exports = {
// sum: sum,
// sub: sub,
// mul: mul,
// di: di,
// }

// 必须要导出才能是外部引用使用
module.exports = {
sum,
sub,
mul,
di
}

// common js 模块化规范

导出测试:

1
2
3
4
5
6
7
8
// 使用 require 导入自己写的工具类

const util = require('./common.js');

console.log(util.sum(1, 2));
console.log(util.sub(1, 2));
console.log(util.mul(1, 2));
console.log(util.di(1, 2));

ES6模块化规范

ES6 使用export 与 import 来实现 导入导入模块

ES6 export 与 import

es6.js

1
2
3
4
5
6
7
8
9
export function getList() {
// 真实业务中 异步获取 数据
console.log("获取数据列表");
}

export function addData() {
// 真实业务中 异步获取 数据
console.log("保存数据");
}

es6-plus.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 新版es6 export 与 import 写法

export default {
addData() {
// 真实业务中 异步获取 数据
console.log("保存数据");
},

getList() {
// 真实业务中 异步获取 数据
console.log("获取数据列表");
}
}

test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用 require 导入自己写的工具类

const util = require('./common.js');

console.log(util.sum(1, 2));
console.log(util.sub(1, 2));
console.log(util.mul(1, 2));
console.log(util.di(1, 2));

console.log("-----------------------------------");

// 注意 默认不支持 ES6 语法 import 需要将其 使用babel 降级 打包成ES5 语法

import {getList, addData} from './es6'
getList();
addData();

console.log("-----------------------------------");

import util2 from './es6-plus'
util2.addData();
util2.getList();

注意:==默认不支持 ES6 语法 import 需要将其 使用babel 降级 打包成ES5 语法后 执行==

8. Webpack

Webpack简介

Webpack是一个前端资源加载/打包工具 它根据模块的依赖关系进行静态分析 将这些模块按照指定规则生成对应的静态资源 Webpack可以将多种静态资源 js css less 转换 成一个静态文件 从而减少了页面的请求

官网:https://webpack.docschina.org/

image-20220925143327685

不仅将静态资源整合 同时也集成 编译 babel 以及 vue 里面的一些功能

Webpack安装及使用

webpack安装

具体操作步骤:

  1. 使用npm install -g webpack-cli 全局安装
  2. webpack -v 查看安装后的版本号

Todo

注意解决 Webpack与webpack-dev-server 版本不兼容问题

1
2
3
4
5
6
7
解决installed问题
PS E:\aPersonalData\vue\Projects\Test\demo-04-webpack> webpack -v
webpack: 5.74.0
webpack-cli: 4.10.0
webpack-dev-server not installed

执行 npm install --save-dev webpack-dev-server

webpack合并JS 打包

具体操作步骤:

  1. 创建一个nodejs项目 目录名为demo-04-webpack npm init -y
  2. 创建一个src目录
  3. 在src中存放两个需要合并的util.js 和 common.js
  4. 准备一个入口文件 main.js 即模块集中进行引用
  5. 准备进行 JS打包 在根目录下定义一个webpack.config.js文件配置打包规则
  6. 执行 webpack 查看效果
  7. 执行 webpack -w 可以实现监听文件改动 实现热更新

README.md

1
2
3
4
5
6
7
8
1. 创建一个nodejs项目 npm init -y
2. 创建一个src目录
3. 在src中存放两个需要合并的util.js 和 common.js
4. 准备一个入口文件 main.js 即模块集中进行引用

// JS打包
5. 在根目录下定义一个webpack.config.js文件配置打包规则
6. 执行webpack查看效果

util.js

1
2
// 相加函数
exports.add = (a, b) => a + b;

common.js

1
2
3
4
5
6
7
// 输出案例
exports.info = function (str) {
// 向控制台输出
console.log(str);
// 向浏览器输出
document.writeln(str);
}

main.js

1
2
3
4
5
6
7
// 导入 util
const util = require('./util');

// 导入 common
const common = require('./common');

common.info("hello world" + util.add(1,32));

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 导入 path模块 nodejs的内置模块
const path = require("path");

// 定义JS 打包规则
module.exports = {
// 入口函数 从哪里开始进行编译打包

entry: "./src/main.js",
// 编译成功后 把内容输出到...
output: {
// 定义输出指定的目录 __dirname 为一个常量 --> 当前项目根目录 产生一个 dist 文件夹
path: path.resolve(__dirname, "./dist"),
// 最终生成 合并的 js 文件存储在dist/bundle.js 文件中
filename: "bundle.js"
}
}

webpack合并CSS 打包

webpack本身只能处理 JavaScript 模块 如需处理其他类型文件 就需要使用loader进行转换 loader可以理解为模块与资源的转换器

在使用 loader 时 需要安装 相关 loader 插件

  • css-loader 是将 css 装载到 JavaScript
  • style-loader 是使得 JavaScript 能识别 css

具体操作步骤:

  1. 执行 npm install --save-dev style-loader css-loader 安装 style-loader 和 css-loader
  2. 修改 配置webpack.config.js
  3. 在src目录下 创建 style.css
  4. 修改 main.js 在第一行 引入 style.css
  5. webpack -w 写进package.json中 运行 npm run dev 编译命令

style.css

1
2
3
body {
background: yellow;
}

main.js

1
2
3
4
5
6
7
8
9
10
// 导入 util
const util = require('./util');

// 导入 common
const common = require('./common');

// 导入css
require("./style.css")

common.info("hello world" + util.add(1,32));

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 导入 path模块 nodejs的内置模块
const path = require("path");

// 定义JS 打包规则
module.exports = {
// 入口函数 从哪里开始进行编译打包

entry: "./src/main.js",
// 编译成功后 把内容输出到...
output: {
// 定义输出指定的目录 __dirname 为一个常量 --> 当前项目根目录 产生一个 dist 文件夹
path: path.resolve(__dirname, "./dist"),
// 最终生成 合并的 js 文件存储在dist/bundle.js 文件中
filename: "bundle.js"
},
module: {
rules: [{
test: /\.css$/, //将项目中 所有的.css 结尾的文件 都进行打包
use: ["style-loader", "css-loader"]
}]
}
}

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "demo-04-webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack -w"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^6.7.1",
"style-loader": "^3.3.1",
"webpack-dev-server": "^4.11.1"
}
}

9. Vue-element-admin

Vue-element-admin简介

Vue-element-admin 是一个 后台前端解决方案 基于vue 与element-ui 实现 它使用了最新的前端技术栈 内置了i18n 国际化解决方案 动态路由 权限认证 提炼了最具典型的业务模型 提供了丰富的功能插件 可以帮助你快速搭建企业级中后台产品原型

官网:https://panjiachen.github.io/vue-element-admin-site/zh/

10. AntDesign

11. package.json